A tech lead in a 15–30 person engineering organisation can turn a routine UI decision into a permanent hosting obligation. The popular default—put the next React product in Next.js App Router—often does exactly that. For an authenticated application whose data already comes from an API, I would start with a Vite-built React client instead, because server rendering adds an execution environment without necessarily improving the user’s experience.
Next.js is the wrong default when the application already lives behind a login
Next.js App Router gives teams React Server Components, server-side rendering, route handlers and a deployment model that can serve public pages efficiently. Those are substantial benefits for a public product whose content needs to arrive before browser JavaScript runs. They are less compelling for a staff console where the first useful screen depends on the user’s identity, permissions and live account data. Rendering a shell on the server does little for that screen if the browser must still wait for authorised API responses before it can show the work.
The cost is not that Next.js is unusually complex; it is that the team now has two places where application code executes. A developer has to know whether a dependency belongs in a Server Component or a Client Component, where a token may be read, and whether a cache could expose one user’s data to another. A bug that was once a browser-to-API request may become a browser-to-Next.js-to-API request. That extra hop can be worthwhile, but it should buy something observable.
I disagree with Scope creep starts after the UI framework choice, not before on timing: the more consequential commitment is often the execution boundary, because changing it later can require work on authentication, caching, deployment and incident response. Selecting React does not commit a team to a Node.js production runtime; selecting a Next.js architecture usually does.
That distinction matters when the same engineers own feature delivery and production support. If a team has 4 frontend contributors, a second runtime does not create another specialist to maintain it. It creates another set of failure modes for the existing contributors to diagnose. I would not adopt Next.js merely to keep the option of server rendering open, because an option that needs routing conventions, runtime deployment and security review has a carrying cost before anyone uses it.
Vite wins for an API-backed console; Next.js wins when the server earns its place
The useful comparison is Vite 7 with React Router 7 versus Next.js App Router with React 19, not “simple” versus “enterprise.” Vite wins when the application is authenticated, its API is already independently deployed, and client-side navigation is acceptable. It produces static assets that can be served through a CDN or existing web server, so releases do not require a new application server. Its cost is that the team must make deliberate choices about routing, data fetching and an HTML fallback for deep links.
Next.js wins when public pages need server-produced HTML, or when server-side access to private data materially simplifies a page. It also wins when route handlers can remove a separate backend-for-frontend service that the team would otherwise operate. Its cost is a production runtime, a server/client code boundary and caching behaviour that developers must understand well enough to test. If those benefits apply to only one public landing page, putting the entire authenticated console in Next.js may be an expensive way to serve that page.
Before deciding, trace one representative route: login, initial data request, navigation, refresh and an expired session. If the existing API uses OAuth 2.0 and OpenID Connect, decide whether the browser or a dedicated backend-for-frontend will hold credentials; a framework choice is not a substitute for that security decision. Prefer a same-origin API path where practical, because it can avoid cross-origin browser restrictions without making UI rendering depend on the API server. Record who owns the HTTP cache headers and which responses must be private.
A small Vite starting point can stay small. The following commands create a TypeScript React application, add routing and request-state libraries, and run the development server:
npm create vite@latest team-console -- --template react-ts cd team-console npm install npm install react-router-dom@7 npm install @tanstack/react-query@5 npm run build npm run dev
Those commands do not establish a production architecture by themselves. Configure the hosting service to return index.html for client routes while serving missing assets as real 404 responses, or refreshes of nested URLs will fail and broken asset links may receive HTML. If the API is on another origin, configure CORS narrowly or provide a same-origin proxy; the Vite development proxy does not exist automatically in production. These are explicit deployment tasks rather than reasons to add a rendering server.
A small team needs firm boundaries, not an internal frontend platform
Choosing a client application does not mean putting every request in a component or sharing state through a single context. I would keep route definitions in React Router 7, remote-data lifecycles in TanStack Query v5, and runtime validation at the API boundary with Zod 4 where responses cannot be trusted to match generated TypeScript types. TypeScript catches incorrect uses of declared types during development; it cannot verify JSON received after deployment.
Organise code around workflows that engineers can own, such as account access or billing review, rather than making separate directories for every hook, button and request. A workflow can expose a small public module to the rest of the application while keeping its query keys and UI details local. That boundary helps a reviewer see whether a change crosses ownership lines, whereas a generic shared-component directory tends to hide why two screens depend on the same code.
I would not begin with module federation, a monorepo migration or independently deployed micro-frontends, because separate release schedules are useful only if teams can also own integration failures. I would also resist turning every workflow into a package: package versioning makes a local code move into a release coordination problem. Scalable React Front-End Architecture for Growth is a useful prompt to plan for change, but I would draw the first boundaries in source code rather than in deployment infrastructure, because source boundaries can be changed without coordinating multiple production releases.
Set a few enforceable rules instead. Use ESLint to prevent workflow modules from importing another workflow’s private files. Require tests for permission-dependent screens with Vitest and React Testing Library, because visual inspection can miss a control that should be absent for one role. Use Playwright for a short path through login, a deep-link refresh and session expiry, because those behaviours cross the browser, routing and API boundaries. Keep API contracts visible through OpenAPI where the backend already publishes a schema, and review schema changes with the feature that consumes them.
A useful first limit is a tunable 6-week trial before creating a new shared package: during that period, collect examples of duplication and identify whether the copies actually change together. The limit is a decision aid, not a rule that forbids reuse. If three screens share a table component but need different selection and permission behaviour, extracting it early may force more configuration into the shared component than leaving the screens separate.
Performance evidence should decide when the default stops working
A client-rendered application can be slow, but a server-rendered application can also be slow; the relevant question is which wait the user experiences. Instrument production routes with the Web Vitals JavaScript library or equivalent Real User Monitoring, segment the results by device and route, and inspect the browser’s Network panel for the slow paths. Google’s published “good” threshold for Interaction to Next Paint is at or below 200 ms at the 75th percentile. That threshold is a reference point, not proof that switching rendering models will improve an interaction delayed by a large table or expensive event handler.
For a first route, also record the number of sequential requests before useful data appears. Treat 3 dependent requests as a proposed investigation trigger, not a universal maximum: the problem is their combined waiting time, and one slow authorisation response may matter more than several parallel requests. If a measured trace shows that the browser waits on avoidable request waterfalls, fix query dependencies or the API shape before moving rendering to a server. If it shows that a large client bundle delays an otherwise ready screen, inspect route-level splitting and dependencies with a bundle analyser before rewriting the application.
Use the same discipline for reliability. Track failed API requests separately from failed static-asset loads, because a server-rendering migration cannot repair an unstable data service. Measure the time from merging a change to a usable production release, because a new runtime that slows releases may cost more than the loading improvement it produces. If public acquisition pages later need pre-rendered HTML, give those routes their own delivery decision instead of retrofitting the authenticated console without evidence.
Test the deployment boundary before approving the framework
Pick one authenticated route this week and write down its request sequence, credential owner, cache policy and deep-link behaviour. Then build the smallest Vite and Next.js versions that use the real API contract, and compare their production deployments rather than their development servers. Approve the server runtime only if it removes a concrete wait or an existing service; otherwise, keep the UI static and spend the saved operational capacity on the route users actually struggle with.


