Co‑locating Nuxt API routes and fine‑tuning per‑route rendering with routeRules
Learn how to place API handlers in server/api, use auto‑imported h3, and control each route’s rendering mode for static, SSR or client‑only builds.
02 Jul 2026, 16:33 UTC

Problem: mixing UI and API in the same Nuxt project
When you build a Nuxt app you often need a public UI and a private API. If the API lives in the same pages folder as the UI, you end up with two concerns in one place, and you lose the ability to control how each route is rendered.
Our thesis
Place all API handlers under server/api and let Nuxt decide the rendering mode for each route with routeRules. This gives you shared types, a clean separation of concerns, and the flexibility to serve static pages, server‑rendered pages or client‑only routes from a single build.
File‑based API routing with auto‑imported h3
Nuxt 3 and later use the Nitro server engine. Any file inside server/api becomes an HTTP endpoint automatically. The file name defines the URL and the HTTP verb.
server/api/hello.get.ts→GET /api/helloserver/api/users.post.ts→POST /api/users- Bracket segments such as
[id]become route parameters, e.g.server/api/posts/[slug].get.ts→/api/posts/hello-world.
Inside a handler you can use defineEventHandler and the H3 helpers readBody, getQuery, getRouterParam, setResponseHeader and createError without importing them manually – they are auto‑imported in server code.
**Example: a post endpoint**
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
// imagine fetching a post
return { title: 'Hello', body: 'World' }
})
Run the app locally with nuxi dev. Call GET /api/posts/hello-world in a browser or with curl and verify a JSON payload and a 200 status.
Per‑route rendering with routeRules
Nuxt lets you mark individual routes as prerendered, cached with swr or isr, or disabled for server‑side rendering altogether. The configuration lives in nuxt.config.ts under the routeRules key.
- Prerender: generate a static HTML file at build time. Use for pages that never change, like
/about. - SWR / ISR: the route is rendered on the server on each request (SSR) and then cached at the edge. Good for frequently updated content such as
/blog. - ssr:false: the route is rendered only on the client after the initial HTML load. Use for admin panels or dashboards that need no SEO.
Example configuration:
export default defineNuxtConfig({
routeRules: {
'/about': { prerender: true },
'/blog/**': { swr: true, swrTtl: 60 },
'/admin/**': { ssr: false }
}
})
After a production build, inspect the .nuxt directory to see which routes were prerendered, which have SWR headers, and which are marked ssr:false. This tells you exactly how each path will be served in the target environment (Node, serverless, edge or static).
Worked example: posts/[slug].get with useFetch
The following handler reads the slug param, loads a post, and returns JSON.
export default defineEventHandler(async (event) => {
const slug = getRouterParam(event, 'slug')
// imagine fetching a post
return { title: 'Hello', body: 'World' }
})
On the page you can fetch the data with useFetch. Because the request runs on the server during the initial render, the response is serialized into the HTML payload.
script setup>
const { data: post } = await useFetch(`/api/posts/${route.params.slug}`)
Verification steps:
- Start the dev server with
nuxi devand open the page for/posts/hello-world. In the Network tab you should see a single request to/api/posts/hello-worldthat returns the JSON and is included in the HTML source. - After a production build, run
nuxi previewand check the rendered HTML. ThepostJSON should appear inside thescript type="application/json"tag, confirming SSR payload. - Inspect the generated Nitro output (e.g.,
.nitrofolder) to ensure the handler was bundled and the route is marked with the intended rendering rule.
Risks: if you place a slow database call directly in the handler, the initial page load will be delayed. Move heavy work to a Nitro hook or a scheduled task instead of the request path.
Trade‑off and limitation
Co‑locating API routes with the rest of the project improves developer experience – you share Typescript interfaces, enjoy auto‑completion, and keep related code together. However, server routes execute in the Nitro runtime, not in the browser, so they cannot use client‑only APIs such as window or localStorage. Also, route‑rule caching depends on the deployment preset; a rule that works for a static target may be ignored on a serverless platform.
To mitigate, keep all secret configuration in runtimeConfig.public only for values you deliberately expose, and move any long‑running or scheduled work out of the request handler into a Nitro hook (nitro:server) or a background job.
Actionable closing
To adopt this pattern in your own Nuxt app:
- Create a
server/apidirectory if it does not exist. - Write a handler file, e.g.
server/api/posts/[slug].get.ts, usingdefineEventHandlerand H3 helpers. - Add the route to
nuxt.config.tsviarouteRules, choosing prerender, swr/isr orssr:falseper path. - Test locally with
nuxi dev, verify the JSON response and that the route appears in the generated.nuxtdirectory. - Build for your target (static, node, serverless) and run
nuxi previewto confirm the rendering behavior matches the rule you set.
By separating API logic and fine‑grained rendering, you get a maintainable codebase, better SEO, and tighter caching without needing multiple repositories or custom server configurations.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.