Prepare for Next.js interview questions grouped by experience level.
Next.js Interview Question & Answers
0-2 Years
Next.js is a React framework that adds routing, server-side rendering, static site generation, and API routes on top of plain React so developers don't have to assemble that tooling themselves. It's built by Vercel and is commonly used for production web applications that need good performance and SEO out of the box.
File-based routing means the folder and file structure inside the pages or app directory automatically defines the application's routes, so a file at pages/about.js becomes the /about route without any manual route configuration. This removes the need for a separate routing library or config file that plain React apps typically require.
The pages directory is the original routing system where each file maps directly to a route and data fetching uses functions like getServerSideProps, while the app directory is the newer routing system built on React Server Components with a different file convention using page.js and layout.js files. The app directory is now the recommended approach for new projects, though both can coexist during a migration.
Server-side rendering generates the HTML for a page on the server for each request before sending it to the browser, rather than the browser building the page entirely with JavaScript after load. This helps pages load with visible content faster and makes them more easily crawlable by search engines.
Static site generation builds the HTML for a page at build time, once, and then serves that same pre-built file to every visitor until the next build. It's well suited to content that doesn't change per request, like a marketing page or blog post, since it's extremely fast to serve.
SSR renders a fresh page on every request, which is useful when content depends on real-time or user-specific data, while SSG renders the page once at build time and reuses it for every visitor, which is faster but only works well for content that doesn't need to change per request. Next.js lets you choose per page which strategy fits its content.
An API route is a file inside the pages/api or app/api directory that runs as a serverless backend endpoint rather than a page, letting you build backend logic like handling a form submission directly within the same Next.js project. This means a Next.js app can serve both its frontend pages and its own lightweight backend API from one codebase.
The Link component enables client-side navigation between pages without a full page reload, which keeps the app feeling fast the way a single-page app does while still benefiting from Next.js's routing and prefetching. It's used in place of a plain HTML anchor tag for internal navigation.
The Image component automatically optimizes images, including resizing, serving modern formats, and lazy loading images that are off-screen, without the developer manually handling that optimization. Using it instead of a plain img tag generally improves page load performance.
getStaticProps is a function you export from a page that fetches data at build time and passes it as props to the page component, which is what powers static site generation for that page. It only runs on the server during the build, never in the browser.
getServerSideProps is a function you export from a page that fetches data on every request before the page renders, which is what powers server-side rendering for that page. Unlike getStaticProps, it runs fresh each time a user visits the page.
A dynamic route uses square brackets in the filename, like pages/posts/[id].js, to match a range of URLs where part of the path is a variable, such as a specific post's ID. The value in the brackets becomes available to the page as a route parameter.
The _app.js file wraps every page in the application, making it the right place to add global layout, shared state providers, or global CSS imports that need to apply across all pages. It's a special file Next.js recognizes automatically rather than something you import manually.
The _document.js file customizes the underlying HTML document structure, like the html and body tags, which is useful for things like adding a custom lang attribute or injecting styles needed before hydration. It runs only on the server and isn't the place for interactive logic.
It means Next.js comes preconfigured with sensible defaults for things like bundling, routing, and code splitting, so a developer can start building without first assembling a custom webpack or Babel setup the way a plain React project often requires. Configuration is still possible when needed, but it isn't required to get started.
Automatic code splitting means Next.js only sends the JavaScript needed for the current page to the browser, rather than bundling the entire application into one large file. This keeps initial page loads faster since users aren't downloading code for pages they haven't visited yet.
Hydration is the process where React attaches event listeners and makes the already-rendered server HTML interactive in the browser, turning static markup into a fully functioning React app. Until hydration completes, the page looks interactive but clicks and other interactions won't yet respond.
The public directory holds static assets like images, fonts, or a favicon that should be served as-is at the root URL path without any processing. A file at public/logo.png becomes accessible at /logo.png directly.
A layout is a special file, layout.js, that wraps the pages nested inside its folder and persists across navigation between those pages, making it a good place for shared UI like a navigation bar or sidebar. Unlike a regular page, a layout doesn't re-render when a user navigates between pages that share it.
A Server Component renders on the server and sends only the resulting HTML to the browser, with no JavaScript for that component shipped to the client, while a Client Component is marked with a 'use client' directive and runs in the browser like a traditional React component with full interactivity. Server Components are the default in the app directory, and you opt into Client Components only where you need things like state or event handlers.
The Head component from next/head lets you inject elements into the page's HTML head, like the title tag or meta description, on a per-page basis. It's commonly used to set unique SEO-relevant tags for each page rather than relying on one static title across the whole site.
Fast Refresh is Next.js's development feature that updates the browser almost instantly when you save a file, preserving component state where possible instead of doing a full page reload. It makes the local development feedback loop noticeably faster than a traditional reload-on-save workflow.
next.config.js is where you customize Next.js's default behavior, like configuring redirects, environment variables, or image domains that are allowed for the Image component. Most projects don't need heavy customization here, but it's the entry point when you do.
A catch-all route uses a filename like [...slug].js to match any number of path segments after a certain point, useful for things like a documentation site where the URL structure is nested and variable. An optional catch-all route, written with double brackets, also matches the base path with no additional segments.
ISR lets a statically generated page be regenerated in the background after a set time interval, without needing a full site rebuild, so content can stay reasonably fresh while still getting most of the performance benefits of static generation. It's set through a revalidate option returned from getStaticProps.
Client-side navigation, triggered through the Link component, swaps out page content without reloading the whole browser tab, keeping shared layout and state intact and making transitions feel faster. A full page reload, like typing a URL directly, reloads everything from scratch including any client-side state.
next/font is a built-in way to load and optimize web fonts, including self-hosting Google Fonts automatically so there's no separate network request to an external font provider at runtime. This helps avoid layout shift and improves loading performance compared to a typical external font link tag.
Middleware is code that runs before a request completes, letting you inspect or modify the request or response, like redirecting a user based on a cookie, before the page itself even starts rendering. It's defined in a single middleware.js file at the root of the project.
A page is a file inside the routing directory that automatically becomes a URL route in the app, while a component is a regular, reusable piece of UI that doesn't automatically map to a route unless it's placed and exported as a page. Pages typically compose several smaller components together.
A variable needs the NEXT_PUBLIC_ prefix to be exposed to browser-side code, while variables without that prefix stay server-side only and are never bundled into the client JavaScript. This distinction matters for keeping secrets like API keys out of the code that ships to users.
A loading.js file automatically shows a loading UI while the content of that route segment is being fetched, using React Suspense under the hood without you needing to wire up Suspense boundaries manually. It's a convention-based way to get instant loading feedback during navigation.
An error.js file defines a fallback UI that automatically displays if an error is thrown while rendering that route segment, functioning similarly to a React error boundary but as a built-in file convention. It helps contain failures to a specific part of the UI rather than crashing the whole app.
A relative import uses ../../ style paths based on the current file's location, which can get unwieldy in deeply nested folders, while a path alias configured in tsconfig.json or jsconfig.json lets you import from a fixed root, like @/components/Button, regardless of the importing file's location. Most Next.js projects set up a path alias early on to keep imports readable.
A route group is a folder wrapped in parentheses, like (marketing), that lets you organize routes and apply a shared layout without that folder name becoming part of the actual URL path. It's purely an organizational tool for the codebase and has no effect on how the route resolves for visitors.
The Metadata API lets you define page titles, descriptions, and other head tags by exporting a metadata object or a generateMetadata function from a page or layout file, replacing the older next/head approach. It works naturally with Server Components since it doesn't require rendering anything in the browser to take effect.
A not-found.js file in the app directory automatically renders when the notFound function is called or a route doesn't match, scoped to whichever segment it's placed in, while the pages directory used a single global pages/404.js file for the entire site's not-found page. The app directory's approach allows more granular, section-specific not-found handling than the older single global file did.
3-6 Years
I'd start by asking how often the content changes and whether it needs to be personalized per user. Content that's the same for everyone and changes rarely, like a marketing page, fits SSG, content that needs to update periodically without a full rebuild fits ISR, and content that must be fresh or user-specific on every request, like a personalized dashboard, needs SSR.
I'd fetch data directly inside the async Server Component using fetch or a database call, without needing a separate exported data-fetching function, since Server Components can be async by default. This is different from getServerSideProps, which required a specific exported function whose return value became props, and it also allows more granular caching control per fetch call.
I'd use middleware to check for a valid session cookie or token on protected routes and redirect unauthenticated users to a login page before the protected page even starts rendering. I'd keep truly sensitive authorization checks on the server side, in Server Components or API routes, rather than relying solely on client-side redirects that a user could bypass.
I'd start by analyzing the bundle with a tool like the built-in bundle analyzer to find which dependencies are contributing the most weight, and look for opportunities to dynamically import heavy components that aren't needed immediately, like a modal or a rich text editor. I'd also check for accidentally importing an entire library when only a small part of it is actually used.
I'd use next/dynamic to import the component, which code-splits it into a separate chunk that only loads when actually rendered, rather than bundling it into the initial page load. For a component with no meaningful content until its data loads, I'd also pass a loading option to show a placeholder while the chunk downloads.
I'd build an API route or a Server Action that accepts the search query and returns matching results, then call it from a Client Component using fetch, debouncing the input so it doesn't fire a request on every keystroke. I'd also add a loading state so the user gets feedback while the request is in flight rather than the UI feeling unresponsive.
I'd put a layout.js at the dashboard route level for the shared sidebar, and a nested layout.js inside the settings folder for the settings-specific sub-navigation, so each layout only wraps the routes that actually need it. This avoids re-rendering the outer sidebar layout when navigating between settings sub-pages, since only the nested layout and page change.
I'd wrap the slow-loading part in a Suspense boundary with its own loading fallback, so the rest of the page can render and become interactive immediately while that specific section streams in once its data resolves. This is one of the main benefits of the app directory's streaming support over the older pattern where one slow data fetch could block the entire page.
I'd make sure the marketing pages use SSG or ISR so they're fast and fully rendered for search engine crawlers, and set proper metadata through the Metadata API or next/head for titles, descriptions, and social sharing tags on each page. For the dashboard pages, since they're typically behind authentication and not meant to be indexed, I'd add a noindex meta tag or exclude them in the robots configuration.
I'd use Next.js's built-in internationalized routing to structure locale-prefixed URLs, like /en/about and /fr/about, and pair it with a translation library to manage the actual string content per locale. I'd make sure the language switcher preserves the current page path so switching languages doesn't drop the user back to the homepage.
I'd look for anything that renders differently on the server versus the client, like using browser-only APIs such as window or Date.now() directly in render logic, or conditionally rendering based on something that isn't available during server rendering. I'd move that kind of logic into a useEffect so it only runs after hydration, or use a dedicated pattern to defer rendering that specific piece until the client has mounted.
I'd define an async function marked with 'use server' that handles the form data directly, either colocated in the same file or in a separate actions file, and pass it to the form's action attribute so it runs on the server when submitted. I'd handle validation and error state by returning a result object the client component can read, since Server Actions can work with or without JavaScript enabled on the client.
I'd cache the fetched data using Next.js's built-in fetch caching with an appropriate revalidate interval so repeated requests within that window don't hit the third-party API again. For anything needing tighter control, I'd add a dedicated caching layer, like storing the response in a key-value store, so multiple concurrent requests don't all trigger separate calls to the rate-limited API at once.
I'd use environment variables defined in .env files per environment, with the deployment platform injecting the correct values for staging and production rather than committing environment-specific secrets to the repo. I'd only prefix variables with NEXT_PUBLIC_ if the browser genuinely needs to read them, keeping server-only values unprefixed so they never leak into client bundles.
I'd migrate route by route, since Next.js supports both directories coexisting during the transition, starting with lower-risk, simpler pages to build familiarity before tackling pages with complex data fetching or heavy client-side interactivity. I'd keep shared logic like API calls in framework-agnostic utility functions so they can be reused by both the old and new pages during the transition period.
I'd use the Image component with the fill or responsive layout options so images adapt to their container without manually specifying fixed dimensions for every uploaded image, and configure the allowed remote image domains in next.config.js if images are hosted externally. I'd also set an appropriate placeholder, like a blurred preview, to avoid layout shift while each image loads.
I'd configure permanent redirects in next.config.js for URL patterns that changed, using 308 status codes so search engines understand the move is permanent and transfer ranking signal to the new URL. For a large number of redirects or ones that need to be updated frequently without a redeploy, I'd move that logic into middleware backed by a lookup table instead.
I'd load it using the Script component with an appropriate loading strategy, like afterInteractive or lazyOnload depending on how time-sensitive the script actually is, rather than a plain script tag that blocks rendering. This lets the page become interactive first while lower-priority third-party scripts load in the background.
I'd use a component testing tool for Client Components' interactive behavior, integration tests hitting API routes directly to verify request and response handling, and end-to-end tests with a tool like Playwright for flows that span multiple pages and real navigation. Server Components need a slightly different testing approach since they can't rely on typical client-side testing utilities that assume a browser-like DOM interaction model.
I'd keep the cart state itself in a Client Component using context or a state library, since cart interactions are inherently client-side and interactive, while letting Server Components handle rendering the initial page content around it. I'd avoid trying to force genuinely interactive state into a Server Component, since that's exactly the boundary the framework expects you to respect.
I'd statically generate the page's main content and render the personalized part as a small Client Component that fetches the user-specific data on the client after the static shell loads, rather than making the entire page dynamic just for one personalized element. This keeps the fast, cacheable benefits of static generation for the majority of the page while still delivering the personalized touch.
I'd centralize the check in middleware that matches on the protected path patterns, redirecting to a login page before the request ever reaches the page component, rather than repeating an authentication check inside each individual page. I'd still keep a server-side check inside genuinely sensitive Server Components or API routes as defense in depth, since middleware alone shouldn't be the only barrier for highly sensitive actions.
I'd default to fetching directly in a Server Component when the data is needed for the initial render, since it avoids an extra network round trip and keeps the fetch logic and credentials server-side. I'd reach for calling an API route from a Client Component mainly when the data needs to update in response to client-side interaction after the initial load, like a live search or a paginated table the user is actively interacting with.
I'd extract the shared pieces into an internal package published to a private registry or managed through a monorepo workspace, so multiple Next.js apps can depend on the same versioned source rather than copying components between codebases. I'd keep the shared package focused on genuinely stable, cross-cutting pieces, since forcing every small UI variation into the shared package tends to make it fragile and hard for any one team to evolve confidently.
6-8 Years
I'd evaluate whether the marketing site and the dashboard app genuinely need to be one deployable unit or would be better split into separate Next.js projects behind a shared domain using rewrites, since a monolithic app with very different rendering needs across sections often suffers slow builds as it grows. If keeping it unified, I'd make heavy use of route groups and careful caching strategy so a change to one section doesn't force unnecessary rebuilding or revalidation of unrelated sections.
I'd set per-fetch revalidate times tuned to how often each specific data source actually changes, rather than applying one blanket caching policy across the whole app, and use on-demand revalidation triggered by a webhook from the CMS or database for content that changes unpredictably but needs to reflect immediately. I'd also be deliberate about which routes opt out of caching entirely for genuinely dynamic, user-specific content, since over-caching a personalized page is a common source of confusing bugs.
I'd start by checking whether a recent code change introduced a blocking, uncached fetch call in a Server Component that used to be cached, since that's a common and easy-to-miss regression in the app directory's fetch caching model. I'd also review deployment platform metrics for cold start frequency on serverless functions, since a change in function size or a new dependency can push a function past a size threshold that affects cold start time.
I'd render the storefront primarily with SSG and ISR for speed and SEO, since product pages benefit heavily from being fast and crawlable, while the account area uses SSR or client-rendered data since it's inherently personalized and not meant to be indexed. I'd keep shared components like the header and footer consistent across both, but let the data-fetching and rendering strategy diverge based on each section's actual requirements.
I'd default to a Server Component for the page as a whole and extract just the interactive pieces, like a button with an onClick handler or a form with local state, into small, focused Client Components nested inside it. This keeps the majority of the page's JavaScript out of the client bundle while still allowing the genuinely interactive parts to work, rather than marking the whole page as a Client Component out of convenience.
I'd build a shared API layer, likely through API routes or a separate backend service, that both the Next.js frontend and the mobile app consume identically, rather than letting the web app's Server Components fetch data in a way that's inaccessible to the mobile client. I'd version that API deliberately once the mobile app depends on it, since mobile app releases can't be redeployed as instantly as a web frontend when a breaking API change ships.
I'd set concrete budgets for metrics like Largest Contentful Paint and JavaScript bundle size, enforced through CI checks that fail a build if a pull request pushes past the threshold, rather than relying on manual review to catch regressions. I'd pair that with real user monitoring in production, since synthetic lab tests in CI don't always reflect what actual users on varied devices and networks experience.
I'd rely as heavily as possible on static generation and CDN caching for the pages expected to receive the traffic spike, since serving from a CDN edge avoids hitting the origin server entirely for cacheable content. For any part that must stay dynamic, like checking real-time inventory, I'd make sure that specific data path is optimized and rate-limited separately so it doesn't become the bottleneck that takes down the whole page.
I'd use Next.js's automatic request deduplication, which caches identical fetch calls made during the same render pass, so multiple components requesting the same resource within one request cycle only trigger one actual network or database call. For data-fetching patterns that don't naturally dedupe, like separate database queries, I'd centralize the fetch into a shared function called once at a higher level and pass the result down rather than each component independently querying the same data.
I'd weigh how much the team benefits from Vercel's tight integration with Next.js features like ISR and edge functions against organizational requirements around infrastructure control, existing cloud provider commitments, or cost at scale. Self-hosting is entirely possible and Next.js supports it, but some features work with less friction on Vercel specifically, so I'd factor that operational tradeoff into the decision rather than treating it as purely a cost question.
I'd fetch the relevant flag values early, ideally in middleware or at the top of a Server Component, and use that to conditionally render different variants within the same route rather than maintaining separate duplicate pages per variant. I'd be careful about how flag evaluation interacts with caching, since a cached static page can't easily reflect a per-user flag without falling back to a more dynamic rendering strategy for that specific route.
I'd profile the build to find whether it's dominated by a large number of statically generated pages, a slow data-fetching step during generation, or an expensive bundling step, since each has a different fix. For a large number of static pages, I'd look at using ISR with on-demand generation instead of pre-rendering everything at build time, which shifts a lot of that cost from build time to runtime, spread out over actual traffic instead of one long build.
8-10 Years
I'd weigh shared design system and authentication needs, which favor staying within the existing app, against the risk of one team's changes affecting build times or stability for every other team sharing that same codebase, which favors separation. Past a certain organizational size, I'd generally favor splitting into separate deployable apps behind a shared gateway or reverse proxy, since a single monolithic Next.js app becomes an increasingly costly shared dependency as more teams build on it independently.
I'd set a deliberate policy of evaluating major version upgrades and new patterns in a dedicated pilot project or a low-risk internal tool first, rather than adopting bleeding-edge changes directly in critical production applications. I'd balance staying reasonably current, since falling too many versions behind eventually makes upgrades much more painful, against the very real cost of chasing every framework change immediately.
I'd frame it around concrete, measurable outcomes the migration unlocks, like meaningfully faster page loads from reduced client JavaScript through Server Components, or developer productivity gains from simplified data fetching patterns, rather than presenting it as a technical modernization for its own sake. I'd propose an incremental migration path with value delivered at each stage, since asking leadership to fund a long, all-at-once rewrite with no interim benefit is a much harder sell and carries more delivery risk.
I'd bake performance checks directly into the shared CI pipeline every team's Next.js app builds on, so it's automatically enforced rather than relying on individual teams to remember to monitor it themselves. I'd also make performance data visible at a leadership-reviewed dashboard level, since teams tend to prioritize what's actually being measured and reported on above them, beyond just what's technically possible to measure.
I'd establish a required review process for any new third-party script or dependency above a certain bundle size impact, and provide a shared, vetted set of common integrations, like analytics or a chat widget, that teams should default to rather than each team independently vetting and integrating the same category of tool. I'd pair this with automated bundle size monitoring in CI so a risky dependency gets flagged before it ships rather than discovered after users are already affected.
I'd centralize the foundational design tokens, shared component library, and accessibility standards, since inconsistency there creates real user-facing and compliance risk, while giving teams flexibility in how they compose those shared pieces for their specific product needs. I'd invest in making the shared component library genuinely pleasant and fast to build with, since teams reliably drift away from a shared system that feels like it's slowing them down rather than helping.
I'd require a standard, audited pattern for accessing secrets, like a shared internal utility that wraps environment variable access with validation, rather than letting individual developers directly reference process.env inline throughout a codebase where a mistake could accidentally expose a server-only value to a Client Component. I'd also add automated checks in CI that scan for common mistakes, like a secret accidentally referenced with the NEXT_PUBLIC_ prefix, before code reaches production.
I'd push for a reasonably synchronized upgrade cadence, since fragmented versions across teams increase the maintenance burden on any shared tooling, libraries, or platform infrastructure that has to support multiple versions simultaneously. I'd still allow some flexibility for teams with a genuinely higher-risk upgrade path, like heavy legacy dependencies, but I'd set an expectation that they can't fall indefinitely far behind the rest of the organization.
I'd require teams to make this tradeoff explicit and data-driven rather than defaulting to whichever pattern is easiest to implement, factoring in actual infrastructure cost of dynamic rendering at their traffic volume against the real business cost of stale content for that specific use case. For most high-traffic, low-personalization content I'd set a strong organizational default toward static generation with revalidation, reserving full dynamic rendering for cases that genuinely require it.
I'd require an owning team for every shared library along with a documented usage list, so a deprecation decision is based on actual current adoption rather than guesswork about who might still depend on it. I'd set a standard sunset process that gives dependent teams a defined migration window and clear replacement guidance, rather than letting a library linger indefinitely because no one wants to own the disruption of retiring it.
I'd build automated accessibility checks directly into the shared CI pipeline so violations block a merge the same way a failing test would, rather than relying on teams to remember manual review under deadline pressure. I'd pair that with a shared, pre-audited component library so teams get accessible defaults for common patterns like modals and forms without having to solve accessibility from scratch for every new feature.
I'd reserve edge rendering for the specific cases where its latency benefit is clearly worth the tradeoff, like authentication checks or geolocation-based redirects that genuinely benefit from running close to the user, rather than defaulting every workload to the edge runtime. I'd be cautious about teams adopting it broadly without understanding its more limited runtime environment, since debugging edge-specific issues can be considerably harder than standard server-side code.
I'd require a shared, standardized error and logging integration that captures both server-side rendering errors and client-side runtime errors into one unified view, rather than letting each team bolt on client-side error tracking alone and missing the server-rendering failures that never reach the browser. I'd make sure the standard integration captures enough context, like which route and rendering strategy was involved, to make server-side failures genuinely debuggable rather than just a generic error count.
I'd require semantic versioning discipline on the shared component library with a clear deprecation process for breaking changes, giving teams a defined migration window rather than pushing breaking updates that land in their build without warning. I'd also maintain a compatibility testing suite that runs the shared library against each major consuming application's build in CI, so a breaking change is caught before it ships rather than discovered by an individual team after the fact.
10+ Years
I'd focus the platform team's early efforts on the handful of shared concerns that create the most cross-team pain if left ungoverned, like the design system, authentication, and deployment pipeline, rather than trying to own everything about how every team builds their app. I'd measure the platform team's success by how much faster and more consistently product teams can ship, not by how much of the codebase the platform team directly controls.
I'd work with them on bringing other teams into the proposal earlier, before the solution is fully baked, since a lot of resistance to platform changes comes from teams feeling a decision was made without their input rather than genuine disagreement with the technical approach. I'd also coach them to lead with the specific problem the change solves for those teams rather than the elegance of the technical solution itself, since that's usually what actually earns support.
I'd translate the platform investment into its effect on that same feature delivery metric leadership already cares about, showing how shared infrastructure reduces the time each team spends solving the same problems independently and how inconsistent, ungoverned implementations tend to slow delivery down later through accumulated technical debt. I'd back this with concrete before-and-after data from teams that have adopted the shared platform versus those still on older, bespoke setups.
I'd plan around a few durable principles, like keeping the platform upgradeable without painful lock-in to any single version's quirks, and treat the specific framework features in use as an implementation detail that should evolve as Next.js itself matures. I'd revisit the vision on a regular cadence rather than treating it as fixed, since a framework moving as quickly as Next.js has in recent years means a rigid multi-year technical plan risks becoming outdated well before it's fully realized.
I'd bring both into a conversation grounded in the actual requirements of each team's use case rather than a debate over caching approach in the abstract, since a disagreement like this often stems from genuinely different needs rather than one engineer simply being wrong. I'd look for a configurable solution that serves both needs if reasonably possible, and if a genuine tradeoff remains that can't serve both equally well, I'd make the call myself with clear reasoning tied to the platform's broader priorities.
I'd give strong senior engineers real ownership of significant architectural decisions for their product area, with me available as a sounding board rather than making the call for them, since building that judgment requires actually living with the consequences of a decision rather than just hearing about tradeoffs secondhand. I'd also create regular venues where these emerging leads present and defend their architectural choices to peers, since that kind of scrutiny builds both their thinking and their communication skills faster than either alone.
I'd start with a pilot on a team that's willing and has a genuine pain point the migration solves, letting that team's concrete success build the case rather than trying to convince every team with a hypothetical pitch upfront. I'd also make the migration path itself as low-friction as possible through shared tooling and codemods, since teams are far more willing to adopt a change that doesn't cost them significant unplanned time away from their own priorities.
I'd lead an immediate, transparent incident response, making sure affected teams have a clear communication channel and a realistic timeline rather than silence while the fix is worked on, since that trust matters as much in the moment as the technical fix itself. I'd follow with a blameless postmortem focused on why the platform's own testing and rollout process didn't catch the issue before it reached every dependent team simultaneously, since a shared library failing wide is usually a process gap as much as a code bug.
I'd centralize the things that create genuine cross-team risk or cost if inconsistent, like authentication, core performance budgets, and the design system, while leaving teams flexibility in how they structure their own routes, data fetching, and component organization within their product area. I'd expect that boundary to shift over time, since what needs central control tends to expand as more of the organization's shared infrastructure and user experience depends on consistency across teams.
I'd make performance and accessibility visible and valued the same way feature delivery already is, through dashboards reviewed at the same cadence as delivery metrics, and by recognizing teams and engineers who invest in getting those things right rather than only celebrating fast feature shipping. I'd also make sure the platform makes the performant, accessible path the easy default, since a team is far more likely to build well when doing so doesn't require extra deliberate effort against the grain of the tools they're given.
I'd avoid mandating immediate standardization on day one given how disruptive that would be layered on top of the reorg itself, and instead give the newly combined team space to jointly evaluate and converge on shared conventions over a defined transition window with my guidance on the key tradeoffs at stake. I'd make sure the eventual standard draws genuinely from what worked well across the merged teams rather than simply defaulting to whichever team's approach happened to be largest going into the reorg.
I'd track outcomes tied to real business impact, like reduced time for product teams to ship a new feature or product surface, measurable improvements in site performance and conversion where that connection is demonstrable, and a reduction in cross-team duplicated effort solving the same infrastructure problems independently. I'd present these as a narrative connecting platform investment to business outcomes rather than internal engineering metrics like the number of shared components built, since that's what actually resonates with executive leadership.
I'd point out concretely how that habit is creating a bottleneck where every significant decision has to route through them personally, which doesn't scale as the platform's surface area and the number of dependent teams keeps growing. I'd encourage them to delegate ownership of a specific upcoming architectural decision fully to a senior engineer on their team, coaching from the sidelines rather than stepping in the moment a different choice than their own instinct starts to emerge.
I'd default strongly toward the framework's own tooling and well-supported ecosystem libraries, reserving custom internal tooling for the specific gaps that genuinely matter to the organization's scale and aren't well served by anything available off the shelf. Custom tooling carries an ongoing maintenance cost that's easy to underestimate at the time it's built, so I'd want a clear, revisited justification for anything the organization commits to owning long term.




