Skematic NYC — Full-Stack Infrastructure & Solutions
Sheet · Field Note
Blog · Nuxt · Vue

Nuxt at Scale: Composition Patterns, Nitro, and Pinia Discipline

Nuxt 3 is easy to start and easy to make a mess of. The patterns that keep a large Nuxt app maintainable — disciplined composables, Nitro on the server, the right rendering mode per route, and restraint with Pinia — are the ones worth getting right early.

5 min read
Published 2026-06-03

Nuxt 3 is one of the better things to happen to Vue in years. Nitro on the server, composables and auto-imports cleaning up boilerplate, and hybrid rendering that lets you mix SSR, ISR, and static per route. It is also easy to make a mess of, because the same auto-imports and ergonomics that make small apps pleasant will let a large one sprawl. The patterns below are the ones we reach for when a Nuxt project is going to live for years and be touched by more than one engineer.

Composables are the unit of architecture

In a well-structured Nuxt app, composables are where cross-component logic lives, and the discipline is keeping them honest. A composable should own one concern, return a stable surface, and be SSR-safe — which mostly means being careful about where state is created. Call useState for anything that needs to survive hydration and stay unique per request; reach for a plain ref only when the state is genuinely local to a single client render. The most common SSR bug we fix is shared module-level state leaking between requests on the server, where one user briefly sees another user's data because a ref was declared at the top of a module instead of inside the composable. It traces back, every time, to a composable that was written as if it only ever ran in the browser. Once the team internalizes that everything runs on the server first, the class of bug disappears.

Data fetching: useFetch, useAsyncData, and not double-fetching

useFetch and useAsyncData are the SSR-safe data primitives, and using them correctly means understanding that they run on the server and serialize their result into the payload so the client does not refetch the same data on hydration. Give every call a stable key, lean on the transform and pick options to shrink the payload to exactly what the component renders, and use server-only endpoints through Nitro when the request needs secrets or talks to a database directly. Payload size is a real performance lever that people forget exists — over-fetching on the server quietly bloats every page, because the whole response gets shipped to the client inside the HTML whether the component uses it or not. Trimming a list endpoint down to the fields the card actually shows can shave real kilobytes off every navigation.

Nitro and edge rendering

Nitro is the part of Nuxt 3 people underestimate. It is a full server with its own route handlers, middleware, and storage layer, and it deploys to Node, to serverless, or to the edge from the same codebase with a configuration change rather than a rewrite. For a content-heavy app, rendering at the edge cuts latency for a global audience without standing up a separate infrastructure project — the same build that runs on a Node box runs on Cloudflare's network. Server routes also give you a place to put API logic that has no business living in the browser: third-party API keys, database queries, webhook handlers. The decision we make per route is the rendering mode, and the rule is simple:

  • Static prerender for marketing and content pages that change on deploy, not per request.
  • ISR for catalog or listing pages that change often but tolerate a short staleness window.
  • SSR for authenticated, personalized, or genuinely dynamic routes that must be fresh.
  • Client-only for heavy dashboard widgets where SSR buys nothing and costs hydration time.

Pinia, used sparingly

Pinia is the Nuxt 3 standard for state, and the failure mode at scale is putting too much in it. The rule we hold to is that Pinia is for genuinely app-wide state — the authenticated user, a cart, a feature-flag set — and route-local state stays in composables and component refs. A store that exists to pass props down two levels is not a store; it is a coupling you will regret the next time you move a component. Keeping the store surface small keeps the app understandable, and it keeps the SSR serialization boundary clean, because everything in a Pinia store gets serialized into the payload and rehydrated on the client. Stuff a store with derived data or large lists and you are paying for it on every single page load, whether or not the current route touches it.

The boring infrastructure that keeps it healthy

The Nuxt apps that stay maintainable have unglamorous things in common: TypeScript in strict mode so the auto-imports do not become a guessing game, a CI pipeline that type-checks and lints on every branch, preview environments per pull request, and image handling through Nuxt Image so nobody ships an unoptimized hero. None of this is exciting, and all of it is the difference between a codebase that ages well and one that calcifies.

Auto-imports and composables make Nuxt fast to write. Discipline about state and rendering is what makes it cheap to maintain.

We have shipped Nuxt across marketing sites, dashboards, commerce storefronts, and internal tools, and the patterns hold across all of them. If you are starting a Nuxt project that needs to scale, or untangling one that already has, our Nuxt development work starts by mapping pages, composables, server routes, and rendering strategy before a line of feature code gets written — because that map is what the rest depends on.

Related service
Nuxt development →

Got a build worth writing about?