This is the page that separates "I built a module once" from "I maintain the tooling twenty developers depend on". None of it is Nuxt-specific cleverness; all of it is the boring machinery that makes shared code safe to depend on.
packages/layers/*, packages/modules/*, apps/*. Apps depend on workspace:* while packages are young, which gives hot reload and one lockfile; publishing to an internal registry comes when consumers live in other repositories.npx nuxt upgrade --dedupe on a cadence, plus subscribing to the GitHub security advisories for nuxt/nuxt.cache, swr or isr; stale HTML and payloads built against the old bundle are a real failure mode.readValidatedBody(event, schema.parse) and getValidatedQuery; never trust a path segment.runtimeConfig.public, app.config or module options, all three of which reach the client or the repository.nuxt-security for CSP and security headers, and keep a dependency review in the release checklist.The workspace layout and the lockfile-based adoption report:
repo/
├─ pnpm-workspace.yaml
├─ packages/
│ ├─ layers/base/ @team/nuxt-layer-base
│ ├─ layers/auth/ @team/nuxt-layer-auth
│ └─ modules/toolkit/ @team/nuxt-toolkit
└─ apps/
├─ shop/ extends: ['@team/nuxt-layer-base', '@team/nuxt-layer-auth']
└─ admin/ extends: ['@team/nuxt-layer-base']
# Which app is on which version of the base layer? The answer to "can we remove it yet?"
rg -n '"@team/nuxt-layer-base": "([^"]+)"' apps/*/package.json -or '$1' --with-filename
Input validation and safe caching in a route a layer ships to everyone:
import { z } from 'zod'
const querySchema = z.object({
q: z.string().min(1).max(100),
page: z.coerce.number().int().min(1).max(50).default(1),
})
export default defineEventHandler(async (event) => {
// Never interpolate raw query values; validate and bound them first.
const { q, page } = await getValidatedQuery(event, querySchema.parse)
return await search(q, page)
})
export default defineNuxtConfig({
routeRules: {
// Personalised: must never be shared between users.
'/account/**': { cache: false },
// Public and identical for everyone: safe to cache, and purge it on deploy.
'/docs/**': { swr: 600 },
},
})
After a deploy, cached HTML and payloads reference chunk names from the previous build. Users on the stale HTML hit missing chunks; Nuxt's emitRouteChunkError reload papers over it, but the correct fix is to version or purge cache keys as part of the deploy. This bites hardest on swr/isr routes behind a CDN.
The specific advisories and the exact versions that fixed them. The guide attributes a server-side remote code execution fix, an unauthorised component-instantiation fix via server-island props, a route-rule authorisation bypass, a server-component denial of service, cross-user payload disclosure on cached pages and a dev-server path disclosure to the 4.4.7 and 4.5.1 releases. Confirm the mapping on the advisories page before quoting version numbers.
docs ↗@team/nuxt-layer-base: tests, typecheck, budget, changelog, migration note, advisory review, adoption report. Keep it under ten lines.getValidatedQuery with a zod schema to a route your layer ships, then send it a malformed query and read the error the consumer would see."Three habits. First, upgrade on a cadence and watch the advisories: Nuxt shipped security releases during 2026, and nuxt upgrade --dedupe plus a CI matrix means upstream breakage arrives as a pull request rather than an incident. Second, remember that everything I ship runs in every app: server routes get validated input and no debug output, island props are untrusted, secrets live only in private runtime config, and personalised routes are never cached without varying on identity. Third, make maintenance cheap: reproduction-first support where every bug becomes a fixture test, conventional commits with a changelog and migration notes, and an adoption report from the apps' lockfiles so deprecations are decided with data."