---
title: "What Nitro and h3 do under your Nuxt server routes · vuejs.school"
description: "A Nuxt server route passes through three layers. What Nuxt, Nitro and h3 each do, from defineEventHandler to storage, caching and deploy presets."
canonical_url: "https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes"
meta:
  "og:description": "A Nuxt server route passes through three layers. What Nuxt, Nitro and h3 each do, from defineEventHandler to storage, caching and deploy presets."
  "og:image": "https://vuejs.school/_og/d/a_~V2hhdCBOaXRybyBhbmQgaDMgZG8gdW5kZXIgeW91ciBOdXh0IHNlcnZlciByb3V0ZXMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_What%2BNitro%2Band%2Bh3%2Bdo%2Bunder%2Byour%2BNuxt%2Bserver%2Broutes%2Cdescription_~QSBOdXh0IHNlcnZlciByb3V0ZSBwYXNzZXMgdGhyb3VnaCB0aHJlZSBsYXllcnMuIFdoYXQgTnV4dCwgTml0cm8gYW5kIGgzIGVhY2ggZG8sIGZyb20gZGVmaW5lRXZlbnRIYW5kbGVyIHRvIHN0b3JhZ2UsIGNhY2hpbmcgYW5kIGRlcGxveSBwcmVzZXRzLg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aGF0LW5pdHJvLWFuZC1oMy1kby11bmRlci15b3VyLXNlcnZlci1yb3V0ZXMi%2Cs_twxbW6bgP0G150rW.png"
  "og:image:alt": "What Nitro and h3 do under your Nuxt server routes · vuejs.school"
  "og:image:height": "600"
  "og:image:type": image/png
  "og:image:width": "1200"
  "og:title": "What Nitro and h3 do under your Nuxt server routes · vuejs.school"
  "twitter:image": "https://vuejs.school/_og/d/a_~V2hhdCBOaXRybyBhbmQgaDMgZG8gdW5kZXIgeW91ciBOdXh0IHNlcnZlciByb3V0ZXMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_What%2BNitro%2Band%2Bh3%2Bdo%2Bunder%2Byour%2BNuxt%2Bserver%2Broutes%2Cdescription_~QSBOdXh0IHNlcnZlciByb3V0ZSBwYXNzZXMgdGhyb3VnaCB0aHJlZSBsYXllcnMuIFdoYXQgTnV4dCwgTml0cm8gYW5kIGgzIGVhY2ggZG8sIGZyb20gZGVmaW5lRXZlbnRIYW5kbGVyIHRvIHN0b3JhZ2UsIGNhY2hpbmcgYW5kIGRlcGxveSBwcmVzZXRzLg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aGF0LW5pdHJvLWFuZC1oMy1kby11bmRlci15b3VyLXNlcnZlci1yb3V0ZXMi%2Cs_twxbW6bgP0G150rW.png"
  "twitter:image:alt": "What Nitro and h3 do under your Nuxt server routes · vuejs.school"
---

> Index of every page on this site: [llms.txt](https://vuejs.school/llms.txt)

## Which layer does what

- **Nuxt** builds the Vue app and drives Nitro. It points Nitro at your `server/` folder, starts it in development and at build time, and includes the route types Nitro generates, which is how `useFetch('/api/products/1')` knows what the route returns.
- **Nitro** is the server builder and runtime. It scans `server/api/`, `server/routes/`, `server/middleware/` and `server/plugins/`, bundles your server code into `.output/`, matches requests to handlers, provides storage and caching, and shapes the output for a host through a preset.
- **h3** is the small HTTP framework inside Nitro. It creates an `event` object for each request and supplies the helpers that read from it and write to it.

Nuxt 4.5 pulls both in through its `@nuxt/nitro-server` package, which depends on `nitropack` ^2.13.4 and `h3` ^1.15.11. Run `npm ls nitropack h3` to see the exact versions in your project.

One detail of the plumbing saves you a request. During server rendering, `useFetch('/api/products/1')` does not go out over the network: Nitro calls the handler directly as a function. In the browser, the same call is a normal HTTP request.

## Write a handler and read the event

Here is a complete route that returns one product or a 404:

*server/api/products/\[id].get.ts*

```
const products = [
  { id: 1, name: 'Desk lamp', price: 39 },
  { id: 2, name: 'Notebook', price: 6 },
]

export default defineEventHandler((event) => {
  const id = Number(getRouterParam(event, 'id'))
  const product = products.find(p => p.id === id)

  if (!product) {
    throw createError({ statusCode: 404, statusMessage: 'Product not found' })
  }

  return product
})
```

The file name does the routing. `[id]` becomes a parameter, which `getRouterParam(event, 'id')` reads as a string (hence the `Number()`). The `.get` suffix limits the route to GET requests, so a POST to the same path never reaches this handler.

`defineEventHandler` comes from h3. It marks the function as a handler and gives it types; it does not change what the function does. What you return becomes the response: an object or an array is sent as JSON, a string as HTML (`text/html`). A thrown `createError` becomes a response with that status code, and any other uncaught error becomes a 500.

The `event` is h3's description of one request:

- `event.method` is the method in capitals ( `GET`).
- `event.path` is the path with its query string ( `/api/products/1?sort=price`).
- `event.headers` holds the request headers.
- `event.context` is a plain object for your own data. h3's router puts route parameters there under `params`.

There are also `event.node.req` and `event.node.res`, the raw Node.js request and response. Outside Node.js and Bun, h3 fakes them with a compatible shim, and the h3 docs ask you to avoid them and prefer the helpers. The last section explains why that advice matters for the next major version.

## Where the auto-imports come from

Nothing in that file is imported, yet every name is typed. The names come from three places:

1. **h3's helpers**: `defineEventHandler`, `getRouterParam`, `getQuery`, `readBody`, `getCookie`, `setCookie`, `setResponseStatus`, `createError`, `sendRedirect` and the rest. Nitro auto-imports all of h3's utilities, so the [h3 v1 docs](https://v1.h3.dev/utils) are the reference.
2. **Nitro's own utilities**: `useStorage`, `useRuntimeConfig`, `defineCachedEventHandler`, `defineCachedFunction` and `defineNitroPlugin`, listed in the [Nitro v2 docs](https://v2.nitro.build/guide/utils).
3. **Your `server/utils/` folder**: every export there is available in every server file.

The app's auto-imports stop at the folder boundary: `ref`, `useFetch` and your `app/composables/` are not auto-imported in `server/`. The [Nuxt docs](https://nuxt.com/docs/4.x/directory-structure/server) describe the server as running in a different context from `app/`, with its own auto-imports and types. Code that both sides need goes in `shared/`, which cannot import Vue or Nitro code.

## Middleware runs before every route

Every file in `server/middleware/` runs on every request, before any route, page requests included. Use it to read something once and put it on `event.context` for the routes:

*server/middleware/session.ts*

```
export default defineEventHandler(async (event) => {
  const token = getCookie(event, 'session')
  if (!token) return

  event.context.userId = await useStorage('data').getItem<string>(`sessions:${token}`)
})
```

Any route can then read the value:

*server/api/me.get.ts*

```
export default defineEventHandler((event) => {
  const userId = event.context.userId
  if (!userId) {
    throw createError({ statusCode: 401, statusMessage: 'Not signed in' })
  }
  return { userId }
})
```

Two rules follow. Keep middleware cheap, because it runs for every page and API request that reaches the server. And never return a value from it: middleware should inspect or extend the context, or throw. A returned value becomes the response, and the route never runs. A bare `return`, as in the session check above, is fine.

## Store data with useStorage

`useStorage()` returns an [unstorage](https://unstorage.unjs.io) instance: one key-value API (`getItem`, `setItem`, `removeItem`, `getKeys`) over many drivers, such as memory, a folder or Cloudflare KV. A mount gives a driver a name, and `useStorage('data')` works through the mount called `data`. That mount writes to `.data/kv` in development and in production with a Node preset; Nitro sets it up when you have not configured `data` yourself.

On a worker there is no disk to write to, so that default does not apply and unconfigured storage lives in memory, gone when the instance stops. Mount a real driver for production and keep the folder for development:

*nuxt.config.ts*

```
export default defineNuxtConfig({
  nitro: {
    storage: {
      data: { driver: 'cloudflare-kv-binding', binding: 'DATA' },
    },
    devStorage: {
      data: { driver: 'fs', base: './.data/kv' },
    },
  },
})
```

`binding: 'DATA'` is the name of the KV namespace binding in your Wrangler configuration. The middleware above does not change. It still calls `useStorage('data')`; only the driver behind the name differs per environment.

## Cache shared responses with defineCachedEventHandler

Some routes do slow work whose result is the same for everyone, like calling a third-party API. Nitro can store the response and reuse it:

*server/api/rates.get.ts*

```
export default defineCachedEventHandler(async () => {
  const rates = await $fetch<Record<string, number>>('https://api.example.com/rates')
  return { rates, fetchedAt: new Date().toISOString() }
}, { maxAge: 60 * 10 })
```

The first request runs the handler and saves the response in the `cache` mount, which is memory in production and a folder in development unless you mount something else. For the next ten minutes (`maxAge` is in seconds) requests get the saved copy. After that, because `swr` (stale-while-revalidate) defaults to `true`, the next request still gets the old copy at once while Nitro refreshes it in the background. Set `swr: false` if callers must wait for fresh data.

The mistake to avoid: according to the [cache docs](https://v2.nitro.build/guide/cache), all incoming request headers are dropped when Nitro handles a cached response, unless you list them in `varies`. A cached handler that reads the session cookie sees no cookie. Cache data that is the same for everyone, and keep per-user routes uncached.

The same cache also works as a route rule in `nuxt.config.ts`, which applies it to a path or a pattern of paths without changing the handler: `routeRules: { '/api/rates': { cache: { maxAge: 600 } } }`.

## Deploy the same code with a preset

A preset is the part of Nitro that knows a host. The default is `node-server`, a Node.js server you start yourself:

```
npx nuxt build
node .output/server/index.mjs
```

The same project as a Cloudflare worker:

```
NITRO_PRESET=cloudflare_module npx nuxt build
```

Nitro bundles the server for the workers runtime and prints the commands to preview and deploy the build when it finishes. On Vercel, Netlify, Cloudflare and a few other providers you can skip the variable: Nitro detects the provider in CI and picks the preset itself.

The third kind of host is no server at all. `npx nuxt generate` pre-renders your pages to HTML in `.output/public`, starting from `/` and following links. Your server routes run during that build, so data fetched while rendering is baked into the pages, and then there is no server. A POST route, a session check or anything that reads the request at runtime does not exist after you deploy. If a page needs one, you need a preset with a server.

## What Nitro 3 and h3 2 will change

Everything above describes Nitro 2 and h3 1. The next generation exists, but as pre-releases:

- **h3 2** is a release candidate. Its [beta announcement](https://h3.dev/blog/v2-beta) describes a rewrite on web standards: `event.req` is a standard `Request`, so you read `event.req.headers.get('accept')` and `await event.req.json()`. The same post says most utilities preserved backward compatibility.
- **Nitro 3** has been in [public beta](https://nitro.build/blog/v3-beta) since 11 March, under a new package name, `nitro`, replacing `nitropack`. It is built on h3 2.
- **Nuxt 5** is where they meet Nuxt. The [Nuxt 4.0 announcement](https://nuxt.com/blog/v4) planned Nuxt 5 with Nitro v3 and h3 v2, and the [Nuxt 4.5 announcement](https://nuxt.com/blog/v4-5) says the team will now focus on stabilising Nuxt 5. That is a plan, not a release.

What you can do now is write handlers that will move cleanly. Use h3's helpers (`getQuery`, `readBody`, `getCookie`, `setResponseStatus`) rather than `event.node.req` and `event.node.res`: read a cookie with `getCookie(event, 'session')`, not by parsing `event.node.req.headers.cookie`. The helpers are the surface meant to carry over; `event.node` is the Node-specific part that h3 2 replaces with web standards.

**Check it stuck**3 questions from the Server routes quiz.

[**Practice now**](https://vuejs.school/learn/nuxt/server-routes?tab=quiz)

**Was this helpful?**

## **Keep reading**

[Release notes · 7 min read**What's new in Nuxt 4.6: nuxt/server, typed $fetch, cookie sessions and Vapor**](https://vuejs.school/community/blog/whats-new-in-nuxt-4-6) [Tutorial · 6 min read**useFetch or $fetch? Picking the right one in Nuxt**](https://vuejs.school/community/blog/usefetch-or-fetch-picking-the-right-one-in-nuxt)

```json
{"@context":"https://schema.org","@graph":[{"@id":"https://vuejs.school/#website","@type":"WebSite","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school/","publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes#webpage","@type":"WebPage","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-08-27","description":"A Nuxt server route passes through three layers. What Nuxt, Nitro and h3 each do, from defineEventHandler to storage, caching and deploy presets.","name":"What Nitro and h3 do under your Nuxt server routes","url":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes","breadcrumb":{"@id":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes#breadcrumb"},"isPartOf":{"@id":"https://vuejs.school/#website"},"potentialAction":[{"@type":"ReadAction","target":["https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes"]}],"primaryImageOfPage":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/#identity","@type":"Organization","description":"A free AI-instructed school for Vue.js. Short tutorials, runnable code and quizzes that adapt to you.","name":"vuejs.school","url":"https://vuejs.school","logo":{"@id":"https://vuejs.school/#logo"}},{"@id":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes#article","dateModified":"2026-10-27T19:31:40.253Z","datePublished":"2026-08-27","description":"A Nuxt server route passes through three layers. What Nuxt, Nitro and h3 each do, from defineEventHandler to storage, caching and deploy presets.","headline":"What Nitro and h3 do under your Nuxt server routes","inLanguage":"en","isAccessibleForFree":true,"thumbnailUrl":"https://vuejs.school/_og/d/a_~V2hhdCBOaXRybyBhbmQgaDMgZG8gdW5kZXIgeW91ciBOdXh0IHNlcnZlciByb3V0ZXMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_What%2BNitro%2Band%2Bh3%2Bdo%2Bunder%2Byour%2BNuxt%2Bserver%2Broutes%2Cdescription_~QSBOdXh0IHNlcnZlciByb3V0ZSBwYXNzZXMgdGhyb3VnaCB0aHJlZSBsYXllcnMuIFdoYXQgTnV4dCwgTml0cm8gYW5kIGgzIGVhY2ggZG8sIGZyb20gZGVmaW5lRXZlbnRIYW5kbGVyIHRvIHN0b3JhZ2UsIGNhY2hpbmcgYW5kIGRlcGxveSBwcmVzZXRzLg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aGF0LW5pdHJvLWFuZC1oMy1kby11bmRlci15b3VyLXNlcnZlci1yb3V0ZXMi%2Cs_twxbW6bgP0G150rW.png","@type":["Article","BlogPosting"],"articleSection":["Deep dive"],"author":{"@id":"https://vuejs.school/#identity"},"image":{"@id":"https://vuejs.school/#/schema/image/1"},"isPartOf":{"@id":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes#webpage"},"mainEntityOfPage":{"@id":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes#webpage"},"publisher":{"@id":"https://vuejs.school/#identity"}},{"@id":"https://vuejs.school/community/blog/what-nitro-and-h3-do-under-your-server-routes#breadcrumb","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","name":"Blog","item":"https://vuejs.school/community/blog","position":1},{"@type":"ListItem","name":"What Nitro and h3 do under your Nuxt server routes","position":2}]},{"@id":"https://vuejs.school/#logo","@type":"ImageObject","caption":"vuejs.school","contentUrl":"https://vuejs.school/apple-touch-icon.png","inLanguage":"en","url":"https://vuejs.school/apple-touch-icon.png"},{"@id":"https://vuejs.school/#/schema/image/1","@type":"ImageObject","contentUrl":"https://vuejs.school/_og/d/a_~V2hhdCBOaXRybyBhbmQgaDMgZG8gdW5kZXIgeW91ciBOdXh0IHNlcnZlciByb3V0ZXMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_What%2BNitro%2Band%2Bh3%2Bdo%2Bunder%2Byour%2BNuxt%2Bserver%2Broutes%2Cdescription_~QSBOdXh0IHNlcnZlciByb3V0ZSBwYXNzZXMgdGhyb3VnaCB0aHJlZSBsYXllcnMuIFdoYXQgTnV4dCwgTml0cm8gYW5kIGgzIGVhY2ggZG8sIGZyb20gZGVmaW5lRXZlbnRIYW5kbGVyIHRvIHN0b3JhZ2UsIGNhY2hpbmcgYW5kIGRlcGxveSBwcmVzZXRzLg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aGF0LW5pdHJvLWFuZC1oMy1kby11bmRlci15b3VyLXNlcnZlci1yb3V0ZXMi%2Cs_twxbW6bgP0G150rW.png","inLanguage":"en","url":"https://vuejs.school/_og/d/a_~V2hhdCBOaXRybyBhbmQgaDMgZG8gdW5kZXIgeW91ciBOdXh0IHNlcnZlciByb3V0ZXMgwrcgdnVlanMuc2Nob29s%2Cc_OgCard.takumi%2Ctitle_What%2BNitro%2Band%2Bh3%2Bdo%2Bunder%2Byour%2BNuxt%2Bserver%2Broutes%2Cdescription_~QSBOdXh0IHNlcnZlciByb3V0ZSBwYXNzZXMgdGhyb3VnaCB0aHJlZSBsYXllcnMuIFdoYXQgTnV4dCwgTml0cm8gYW5kIGgzIGVhY2ggZG8sIGZyb20gZGVmaW5lRXZlbnRIYW5kbGVyIHRvIHN0b3JhZ2UsIGNhY2hpbmcgYW5kIGRlcGxveSBwcmVzZXRzLg%2Ceyebrow_~RGVlcCBkaXZlIMK3IDcgbWluIHJlYWQ%2Ctint_sky%2Cp_Ii9jb21tdW5pdHkvYmxvZy93aGF0LW5pdHJvLWFuZC1oMy1kby11bmRlci15b3VyLXNlcnZlci1yb3V0ZXMi%2Cs_twxbW6bgP0G150rW.png"}]}
```