Nuxt 3 Server Routes with Nitro: Co-located Private API Architecture
Design pattern for Nuxt 3 server routes with Nitro, covering trust boundaries, runtimeConfig separation, operational checks, and when to scale beyond co-location.
10 Jan 2026, 09:16 UTC

Problem: server secrets without a separate backend
A Nuxt 3 application frequently needs to access private data, read secrets, or call internal services while maintaining SEO-friendly rendering and minimal client-side JavaScript. The useful takeaway is that Nuxt Server Routes, powered by Nitro, provide a co-located private API surface without requiring a distinct backend service, provided trust boundaries are explicit and consistently enforced.
Requirements
- Server-side data access with secrets that must never be transmitted to the browser.
- Minimal client JavaScript and rendering that remains SEO-friendly.
- API code co-located with the user interface for a single deploy artifact.
- Same-origin requests from client pages to internal handlers, routed by Nitro without leaving the origin.
Smallest suitable design
Place handlers under server/api or server/routes. Nitro automatically mounts these directories and executes them in the Node or serverless runtime. Keep private logic in server-only files and server-only composables; they are never bundled to the client.
export default defineNuxtConfig({ runtimeConfig: { privateKey: process.env.PRIVATE_KEY, public: { appName: 'demo' } } })Client pages call internal routes via $fetch. The request is routed by Nitro without leaving the same origin. Server-only code remains on the server.
Trust and data boundaries
Nitro enforces the execution context: server routes run in Node or serverless runtime and can read runtimeConfig private values. Client code only receives runtimeConfig.public. To enforce the boundary:
- Never import a server-only utility in a component; the build will fail or code may leak to the client.
- Shape responses for the browser. Do not return internal fields, error stacks, or raw config values.
- Private runtimeConfig values must not be referenced in client code. Accidental reference causes build errors or, worse, secret leakage.
Operational checks
After deploy, run the following checks to maintain reliability and security:
- Health check: create a server/api/health.get.ts handler that returns status and runtime information; target from monitoring systems.
- Logs: log errors server-side without exposing stacks to clients; return generic error shapes from handlers.
- Cold start latency: monitor Nitro cold start for serverless targets; high variance may indicate a need for provisioned concurrency or response caching.
- Config load: validate that required environment variables are present at startup; missing private values should cause the process to fail fast.
Failure modes and design change triggers
- Unhandled errors in server routes return 500s to the client. Wrap handlers and return controlled error shapes.
- Accidental exposure of private runtimeConfig via public config. Audit config usage regularly.
- Increased latency from server-side rendering on every request. Use Nitro caching or client-side fetch with stale-while-revalidate for read-heavy data.
- Server routes are unauthenticated by default. Add auth, rate limiting, and input validation before exposing beyond internal use.
Keep this co-located design while requirements remain read-heavy, single-region, and the trust boundary is the same origin. Move to a separate API service if requirements grow to multi-region writes with strong consistency, persistent WebSockets or long-lived connections, strict isolation with separate SLAs, or independent deploy cadence for the API beyond the Nuxt UI.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.