Subchapter 3.6
references/core-routing.mdMarkdown6 KBView on GitHub
Nitro maps files in routes/ and api/ to HTTP routes at build time (no runtime router). Handlers receive an H3 v2 and should the response body or an error.
eventimport { defineHandler } from "nitro";
export default defineHandler((event) => {
return { hello: "world" };
});defineHandler gives type inference. A plain (event) => ... function also works. The event is web-standard based:
event.req // web Request
event.res // response init (headers, status)
event.url // URL object (event.url.pathname, event.url.searchParams)
event.path // request path
event.method // HTTP method
event.context // mutable per-request context (params, custom data)
event.context.params // route paramsRead the body with native Request methods (H3 v2 dropped readBody):
const json = await event.req.json();
const text = await event.req.text();
const form = await event.req.formData();Files in api/ (served under /api) or routes/ (served under /) become routes. One handler per file.
routes/
hello.ts -> /hello
api/
test.ts -> /api/test
[org]/
[repo]/
index.ts -> /api/:org/:repo
issues.ts -> /api/:org/:repo/issuesAppend the method to match only that verb (get, post, put, delete, patch, head, options…):
import { defineHandler } from "nitro";
export default defineHandler(async (event) => {
const body = await event.req.json();
return { created: body };
});export default defineHandler((event) => {
const { name } = event.context.params!;
return `Hello ${name}!`;
});[a]/[b] (not in one filename).[...name].ts captures the rest of the path (includes /).[...].ts matches all otherwise-unmatched routes.(admin)/ group files without affecting the URL..dev, .prod, or .prerender (after the method suffix) to include a handler only in that build: test.get.prod.ts.ignore: ["routes/**/_*"] config excludes files from scanning.Files in middleware/ run on every request before route matching. They modify the event and must not return (returning ends the request).
import { defineHandler } from "nitro";
export default defineHandler((event) => {
event.context.user = { name: "Nitro" };
});Control execution order with numeric prefixes (01.logger.ts, 02.auth.ts — pad to keep string sort correct). Scope manually with event.url.pathname, or register route-scoped middleware in config:
export default defineConfig({
handlers: [
{ route: "/api/**", handler: "./middleware/api-auth.ts", middleware: true },
],
});Register handlers/middleware in config in addition to (or instead of) the filesystem:
export default defineConfig({
routes: {
"/api/hello": "./routes/api/hello.ts",
"/api/custom": { handler: "./routes/custom.ts", method: "POST", lazy: true },
},
handlers: [
{ route: "/blog/**", handler: "./handlers/blog.ts", method: "get" },
],
});Handler options: handler, method, lazy, middleware, format ("web" | "node"), env.
Throw HTTPError (replaces v2 createError):
import { defineHandler, HTTPError } from "nitro";
export default defineHandler((event) => {
const user = findUser(event.context.params!.id);
if (!user) {
throw new HTTPError({ status: 404, message: "User not found" });
}
return user;
});In dev, browsers (Accept: text/html) get an HTML error page; production always returns JSON. Customize with errorHandler config pointing to a module that exports defineErrorHandler((error, event) => Response).
Apply per-route behavior (caching, headers, redirects, proxy, auth) by glob pattern. Rules merge least-specific to most-specific; set a rule to false to disable an inherited one.
import { defineConfig } from "nitro";
export default defineConfig({
routeRules: {
"/blog/**": { swr: true }, // stale-while-revalidate (cache)
"/blog/posts/**": { swr: 600 }, // swr with maxAge seconds
"/api/data/**": { cache: { maxAge: 60 } }, // full cache options
"/api/realtime/**": { cache: false }, // disable caching
"/assets/**": { headers: { "cache-control": "s-maxage=0" } },
"/api/v1/**": { cors: true, headers: { "access-control-allow-methods": "GET" } },
"/old-page": { redirect: "/new-page" }, // 307 by default
"/legacy": { redirect: { to: "https://example.com/", status: 308 } },
"/old-blog/**": { redirect: "https://blog.example.com/**" }, // wildcard preserves suffix
"/proxy/**": { proxy: "https://api.example.com/**" },
"/admin/**": { basicAuth: { username: "admin", password: "secret" } },
"/about": { prerender: true },
"/isr/**": { isr: 60 }, // Vercel ISR
},
});Route rule keys: headers, redirect, proxy, cors, cache, swr, static, basicAuth, prerender, isr. swr: true is shorthand for cache: { swr: true }; swr: <n> adds maxAge: <n>. Rules can also be supplied via runtimeConfig.nitro.routeRules for env-var overrides without rebuilding.
send* helpers are gone.event.req.json()/text()/formData() instead of v2 readBody.event.context.params; the ! non-null assertion is common in TS.inlineDynamicImports: true to bundle into one file).