Add a Validated Nitro API Route in Nuxt and Consume It with useFetch
Expose a stable JSON endpoint from a Nuxt server route, validate input with createError, and consume it with useFetch without hydration mismatches.
16 Sept 2026, 03:51 UTC

The problem: server data that breaks SSR or leaks stale responses
Adding an API endpoint to a Nuxt application is straightforward, but two failure modes are common. The first is a hydration mismatch: the server renders one value, the client fetches another, and the browser console fills with warnings. The second is a stale response: useFetch deduplicates requests by a key, so reusing the same key for different inputs silently returns the first result. A validated Nitro server route paired with a correctly scoped useFetch key avoids both.
Prerequisites
- An existing Nuxt project running the Nitro server engine.
- A validation strategy: manual type checks or a schema library such as Zod or Valibot.
- Awareness of your target runtime. Server routes are bundled for the deployment target; Node.js and edge runtimes differ in available built-in modules.
- Confirmation of your project layout. Nuxt 3 places server routes under
server/api. Nuxt 4 introduced anapp/directory for application code; verify the exact server directory before copying paths.
Create the server route
Server routes live in the server/api directory and export a default event handler. Nitro maps the file name and HTTP method suffix to the URL. For example, server/api/report.post.ts handles POST /api/report.
// server/api/report.post.ts
export default defineEventHandler(async (event) => {
const body = await readBody(event)
// Manual validation example
if (
!body ||
typeof body.startDate !== 'string' ||
typeof body.endDate !== 'string'
) {
throw createError({
statusCode: 400,
statusMessage: 'Bad Request',
data: {
message: 'startDate and endDate must be ISO date strings'
}
})
}
// Return a stable shape
return {
ok: true,
data: {
rows: [],
generatedAt: new Date().toISOString()
}
}
})
Replace the manual checks with a schema parser if you prefer. The important detail is the error shape: createError with a statusCode and a data payload gives the client a predictable structure for both success and failure.
Consume the route with useFetch
In a page component, call useFetch during setup. The key option controls deduplication and payload caching. Derive the key from the request identity—not a random value—so server rendering and client navigation agree.
<script setup>
const requestBody = {
startDate: '2026-01-01',
endDate: '2026-01-31'
}
const { data, error, refresh } = await useFetch('/api/report', {
key: `report-${requestBody.startDate}-${requestBody.endDate}`,
method: 'POST',
body: requestBody
})
</script>
If the request body changes, update the key accordingly. Reusing a static key across different bodies causes useFetch to return the first cached response on subsequent navigations.
Expected checks
Start the development server, then verify the endpoint directly. Run these commands in your terminal from the project root.
# Valid request
curl -X POST http://localhost:3000/api/report \
-H "Content-Type: application/json" \
-d '{"startDate":"2026-01-01","endDate":"2026-01-31"}'
A successful response returns HTTP 200 and a JSON body matching the documented shape. Next, send an invalid payload.
# Invalid request
curl -i -X POST http://localhost:3000/api/report \
-H "Content-Type: application/json" \
-d '{"startDate":123}'
Expect HTTP 400 and a JSON error object containing the message field defined in the handler.
To confirm server-side rendering, fetch the consuming page with JavaScript disabled or use view-source: in the browser. The data should appear in the HTML. During client navigation, open the browser console and look for hydration mismatch warnings or duplicate-key messages.
Recovery options
- Wrong data or stale response: If the page shows outdated content after changing inputs, verify that the
useFetchkey changes with the request. Callrefresh()to force a new fetch when the key stays the same. - Hydration mismatch: Ensure headers, cookies, and time-dependent values such as
generatedAtare handled consistently. If the server and client disagree, the mismatch usually originates from a value that changes between render passes. - Route not found: Confirm the file suffix matches the HTTP method and that the path aligns with your Nuxt version layout. Remove or rename the route file to roll back the endpoint.
Limitations and version notes
Server routes execute only on the server. Never import client-only plugins into a handler, and avoid returning secrets in the payload. Edge runtimes restrict Node.js built-ins such as fs, path, and crypto; using them requires a Node-compatible deployment target.
Nuxt 4 changed the default application directory structure. Before following path examples, confirm whether your project uses the Nuxt 3 server/api layout or a Nuxt 4 variant. Run a production build and preview to verify the route behaves identically outside development.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.