Turn Nuxt Server Routes into a Lightweight BFF Layer
Use Nuxt 3 server routes as a Backend-for-Frontend (BFF) to hide API secrets, aggregate data, and reduce client-side complexity without needing a separate backend service.
13 Jan 2026, 03:32 UTC

The Problem
Modern single-page apps often need data from multiple third-party services. Exposing those services directly from the browser forces you to ship API keys in the client bundle, deal with CORS (Cross-Origin Resource Sharing) restrictions, and duplicate data-shaping logic across multiple pages. You are often left choosing between a full-blown backend architecture or a thin client-side wrapper that leaks secrets.
Why Nuxt Server Routes Are a Good Fit
Nuxt 3’s server/api folder creates file-based API endpoints that run on Nitro, the framework’s unified server engine. Because these routes execute in a Node.js or edge environment, they can act as a Backend-for-Frontend (BFF) layer. This allows you to:
- Store credentials in
runtimeConfigand keep them entirely out of the client bundle. - Aggregate data from several services before returning a single, optimized JSON payload to the frontend.
- Apply server-side validation or rate limiting without managing separate infrastructure.
- Deploy the same build to serverless or edge targets, letting Nitro handle the routing automatically.
Building a Proxy BFF
The following example demonstrates a catch-all proxy that forwards requests to an external API while keeping the API key secure on the server.
1. Configure Runtime Secrets
In nuxt.config.ts, add the secret under runtimeConfig. Do not place it inside the public object, as that would expose it to the browser.
import { defineNuxtConfig } from 'nuxt'
export default defineNuxtConfig({
runtimeConfig: {
apiSecret: process.env.API_SECRET, // Server-only
public: {
apiBase: 'https://api.example.com'
}
}
})
Define API_SECRET=your_key_here in your .env file. Nuxt ensures this value is only accessible during server-side execution.
2. Create the Catch-All Route
Create a file at server/api/proxy/[...slug].ts. The [...slug] syntax creates a catch-all route that captures all paths following /api/proxy/.
import { defineEventHandler, getQuery, createError } from 'h3'
export default defineEventHandler(async (event) => {
const config = useRuntimeConfig()
// Extract the path after /api/proxy/
const path = event.node.req.url.replace(/^\/api\/proxy\//, '')
const target = `${config.public.apiBase}/${path}`
// Basic validation: reject requests without query parameters
const query = getQuery(event)
if (!Object.keys(query).length) {
throw createError({
statusCode: 400,
statusMessage: 'Missing query parameters'
})
}
try {
const response = await fetch(target, {
method: event.node.req.method,
headers: {
'Authorization': `Bearer ${config.apiSecret}`,
'Content-Type': 'application/json'
},
body: event.node.req.method !== 'GET' ? JSON.stringify(await readBody(event)) : undefined
})
if (!response.ok) {
throw createError({
statusCode: response.status,
statusMessage: 'External API error'
})
}
return await response.json()
} catch (err) {
throw createError({
statusCode: 500,
statusMessage: err.message || 'Internal Server Error'
})
}
})
3. Verification and Testing
- Local Test: Run
npx nuxi devand navigate to/api/proxy/users?id=123. Verify the response matches the external API. - Secret Leak Check: Run
npx nuxi build. Search the.output/publicdirectory for yourAPI_SECRETvalue. It should not be present. - Deployment Check: Inspect
.output/serverto see how Nitro has bundled the handler for your specific target (e.g., Node or Vercel).
Trade-offs & Limitations
- Resource Consumption: Server routes execute on every request. Heavy data aggregation or complex transformations can increase SSR latency and server costs.
- Runtime Constraints: If deploying to Edge runtimes (like Cloudflare Workers), avoid Node-specific APIs (e.g.,
fsorpath) as they are not supported in those environments. - Security Surface: A proxy route can become an open gateway if not properly guarded. Always validate inputs and consider adding authentication middleware to the
server/apiroutes to prevent unauthorized external access to your proxy. - State Management: These routes are stateless. If you need to maintain sessions or complex state across requests, you will still need a database or a caching layer like Redis.
Next Steps
Once your proxy is operational, you can expand the BFF layer by implementing response caching using Nitro's defineCachedEventHandler or by aggregating multiple API calls into a single response to reduce the number of round-trips the client must make. For small-to-medium projects, this approach provides a secure, zero-config alternative to maintaining a separate backend service.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.