Routing

Nitro supports filesystem routing to automatically map files to routes. By combining code-splitting with compiled routes, it removes the need for a runtime router, leaving only minimal compiled logic.

Request handler

Nitro request handler is a function accepting an event object, which is a H3Event object.

import type { H3Event } from "nitro";

export default (event: H3Event) => {
  return "world";
}

Filesystem routing

Nitro supports file-based routing for your API routes (files are automatically mapped to h3 routes). Defining a route is as simple as creating a file inside the api/ or routes/ directory.

You can only define one handler per files and you can append the HTTP method to the filename to define a specific request method.

routes/
  api/
    test.ts      <-- /api/test
  hello.get.ts   <-- /hello (GET only)
  hello.post.ts  <-- /hello (POST only)
vite.config.ts

You can nest routes by creating subdirectories.

routes/
  api/
    [org]/
      [repo]/
        index.ts   <-- /api/:org/:repo
        issues.ts  <-- /api/:org/:repo/issues
      index.ts     <-- /api/:org
package.json

Route Groups

In some cases, you may want to group a set of routes together in a way which doesn't affect file-based routing. For this purpose, you can put files in a folder which is wrapped in parentheses ( and ).

For example:

routes/
  api/
    (admin)/
      users.ts   <-- /api/users
      reports.ts <-- /api/reports
    (public)/
      index.ts   <-- /api
package.json
The route groups are not part of the route definition and are only used for organization purposes.

Static routes

First, create a file in routes/ or routes/api/ directory. The filename will be the route path.

Then, export a fetch-compatible function:

routes/api/test.ts
import { defineHandler } from "nitro";

export default defineHandler(() => {
  return { hello: "API" };
});

Dynamic routes

Single param

To define a route with params, use the [<param>] syntax where <param> is the name of the param. The param will be available in the event.context.params object or using the getRouterParam utility.

routes/hello/[name].ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  const { name } = event.context.params;

  return `Hello ${name}!`;
});

Call the route with the param /hello/nitro, you will get:

Response
Hello nitro!

Multiple params

You can define multiple params in a route using [<param1>]/[<param2>] syntax where each param is a folder. You cannot define multiple params in a single filename of folder.

routes/hello/[name]/[age].ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  const { name, age } = event.context.params;

  return `Hello ${name}! You are ${age} years old.`;
});

Catch-all params

You can capture all the remaining parts of a URL using [...<param>] syntax. This will include the / in the param.

routes/hello/[...name].ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  const { name } = event.context.params;

  return `Hello ${name}!`;
});

Call the route with the param /hello/nitro/is/hot, you will get:

Response
Hello nitro/is/hot!

Specific request method

You can append the HTTP method to the filename to force the route to be matched only for a specific HTTP request method, for example hello.get.ts will only match for GET requests. You can use any HTTP method you want.

Supported methods: get, post, put, delete, patch, head, options, connect, trace.

// routes/users/[id].get.ts
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const { id } = event.context.params;

  // Do something with id

  return `User profile!`;
});

Catch-all route

You can create a special route that will match all routes that are not matched by any other route. This is useful for creating a default route.

To create a catch-all route, create a file named [...].ts.

routes/[...].ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  return `Hello ${event.url}!`;
});

Environment specific handlers

You can specify for a route that will only be included in specific builds by adding a .dev, .prod or .prerender suffix to the file name, for example: routes/test.get.dev.ts or routes/test.get.prod.ts.

The suffix is placed after the method suffix (if any):

routes/
  env/
    index.dev.ts       <-- /env (dev only)
    index.get.prod.ts  <-- /env (GET, prod only)
You can specify multiple environments or specify a preset name as environment using programmatic registration of routes via routes config.

Ignoring files

You can use the ignore config option to exclude files from route scanning. It accepts an array of glob patterns relative to the server directory.

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  ignore: [
    "routes/api/**/_*",   // Ignore files starting with _ in api/
    "middleware/_*.ts",    // Ignore middleware starting with _
    "routes/_*.ts",       // Ignore root routes starting with _
  ],
});

Programmatic route handlers

In addition to filesystem routing, you can register route handlers programmatically using the routes config option.

routes config

The routes option allows you to map route patterns to handlers:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routes: {
    "/api/hello": "./server/routes/api/hello.ts",
    "/api/custom": {
      handler: "./server/routes/api/hello.ts",
      method: "POST",
      lazy: true,
    },
    "/virtual": {
      handler: "#virtual-route",
    },
  },
});

Each route entry can be a simple string (handler path) or an object with the following options:

OptionTypeDescription
handlerstringPath to event handler file or virtual module ID
methodstringHTTP method to match (get, post, etc.)
lazybooleanUse lazy loading to import handler
format"web" | "node"Handler type. "node" handlers are converted to web-compatible
envstring | string[]Environments to include this handler ("dev", "prod", "prerender", or a preset name)

handlers config

The handlers array is useful for registering middleware with control over route matching:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  handlers: [
    {
      route: "/api/**",
      handler: "./server/middleware/api-auth.ts",
      middleware: true,
    },
  ],
});

Each handler entry supports the following options:

OptionTypeDescription
routestringHTTP pathname pattern (e.g., /test, /api/:id, /blog/**)
handlerstringPath to event handler file or virtual module ID
methodstringHTTP method to match (get, post, etc.)
middlewarebooleanRun handler as middleware before route handlers
lazybooleanUse lazy loading to import handler
format"web" | "node"Handler type. "node" handlers are converted to web-compatible
envstring | string[]Environments to include this handler ("dev", "prod", "prerender", or a preset name)

Middleware

Nitro route middleware can hook into the request lifecycle.

A middleware can modify the request before it is processed, not after.

Middleware are auto-registered within the middleware/ directory.

middleware/
  auth.ts
  logger.ts
  ...
routes/
  hello.ts

Simple middleware

Middleware are defined exactly like route handlers with the only exception that they should not return anything. Returning from middleware behaves like returning from a request - the value will be returned as a response and further code will not be ran.

middleware/auth.ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  // Extends or modify the event
  event.context.user = { name: "Nitro" };
});

Middleware in middleware/ directory are automatically registered for all routes. If you want to register a middleware for a specific route, see Object Syntax Event Handler.

Returning anything from a middleware will close the request and should be avoided! Any returned value from middleware will be the response and further code will not be executed however this is not recommended to do!

Route Meta

You can define route handler meta at build-time using defineRouteMeta macro in the event handler files.

This feature is currently experimental.
routes/api/test.ts
import { defineRouteMeta } from "nitro";
import { defineHandler } from "nitro";

defineRouteMeta({
  openAPI: {
    tags: ["test"],
    description: "Test route description",
    parameters: [{ in: "query", name: "test", required: true }],
  },
});

export default defineHandler(() => "OK");
This feature is currently usable to specify OpenAPI meta. See swagger specification for available OpenAPI options.

Execution order

Middleware are executed in directory listing order.

middleware/
  auth.ts <-- First
  logger.ts <-- Second
  ... <-- Third

Prefix middleware with a number to control their execution order.

middleware/
  1.logger.ts <-- First
  2.auth.ts <-- Second
  3.... <-- Third
Remember that file names are sorted as strings, thus for example if you have 3 files 1.filename.ts, 2.filename.ts and 10.filename.ts, the 10.filename.ts will come after the 1.filename.ts. To avoid this, prefix 1-9 with a 0 like 01, if you have more than 10 middleware in the same directory.

Request filtering

Middleware are executed on every request.

Apply custom logic to scope them to specific conditions.

For example, you can use the URL to apply a middleware to a specific route:

middleware/auth.ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  // Will only execute for /auth route
  if (event.url.pathname.startsWith('/auth')) {
    event.context.user = { name: "Nitro" };
  }
});

Route-scoped middleware

You can register middleware for specific route patterns using the handlers config with the middleware option and a specific route:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  handlers: [
    {
      route: "/api/**",
      handler: "./server/middleware/api-auth.ts",
      middleware: true,
    },
  ],
});

Unlike global middleware (registered in the middleware/ directory which match /**), route-scoped middleware only run for requests matching the specified pattern.

Error handling

You can use the utilities available in H3 to handle errors in both routes and middlewares.

The way errors are sent back to the client depends on the environment. In development, requests with an Accept header of text/html (such as browsers) will receive a HTML error page. In production, errors are always sent in JSON.

This behaviour can be overridden by some request properties (e.g.: Accept or User-Agent headers).

Code splitting

Nitro creates a separate chunk for each route handler. Chunks load on-demand when first requested, so /api/users doesn't load code for /api/posts.

See inlineDynamicImports to bundle everything into a single file.

Route rules

Nitro allows you to add logic at the top-level for each route of your configuration. It can be used for redirecting, proxying, caching, authentication, and adding headers to routes.

It is a map from route pattern (following rou3) to route options.

When cache option is set, handlers matching pattern will be automatically wrapped with defineCachedEventHandler. See the cache guide to learn more about this function.

swr: true|number is shortcut for cache: { swr: true, maxAge: number }

You can set route rules in the nitro.routeRules options.

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/blog/**': { swr: true },
    '/blog/**': { swr: 600 },
    '/blog/**': { static: true },
    '/blog/**': { cache: { /* cache options*/ } },
    '/assets/**': { headers: { 'cache-control': 's-maxage=0' } },
    '/api/v1/**': { cors: true, headers: { 'access-control-allow-methods': 'GET' } },
    '/old-page': { redirect: '/new-page' },
    '/old-page/**': { redirect: '/new-page/**' },
    '/proxy/example': { proxy: 'https://example.com' },
    '/proxy/**': { proxy: '/api/**' },
    '/admin/**': { basicAuth: { username: 'admin', password: 'supersecret' } },
  }
});

Rule merging and overrides

Route rules are matched from least specific to most specific. When multiple rules match a request, their options are merged, with more specific rules taking precedence.

You can use false to disable a rule that was set by a more general pattern:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/api/cached/**': { swr: true },
    '/api/cached/no-cache': { cache: false, swr: false },
    '/admin/**': { basicAuth: { username: 'admin', password: 'secret' } },
    '/admin/public/**': { basicAuth: false },
  }
});

Headers

Set custom response headers for matching routes:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/api/**': { headers: { 'cache-control': 's-maxage=60' } },
    '**': { headers: { 'x-powered-by': 'Nitro' } },
  }
});

CORS

Enable CORS headers with the cors: true shortcut. This sets access-control-allow-origin: *, access-control-allow-methods: *, access-control-allow-headers: *, and access-control-max-age: 0.

You can override individual CORS headers using headers:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/api/v1/**': {
      cors: true,
      headers: { 'access-control-allow-methods': 'GET' },
    },
  }
});

Redirect

Redirect matching routes to another URL. Use a string for a simple redirect (defaults to 307 status), or an object for more control:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    // Simple redirect (307 status)
    '/old-page': { redirect: '/new-page' },
    // Redirect with custom status
    '/legacy': { redirect: { to: 'https://example.com/', status: 308 } },
    // Wildcard redirect — preserves the path after the pattern
    '/old-blog/**': { redirect: 'https://blog.example.com/**' },
  }
});

Proxy

Proxy requests to another URL. Supports both internal and external targets:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    // Proxy to exact URL
    '/api/proxy/example': { proxy: 'https://example.com' },
    // Proxy to internal route
    '/api/proxy/**': { proxy: '/api/echo' },
    // Wildcard proxy — preserves the path after the pattern
    '/cdn/**': { proxy: 'https://cdn.jsdelivr.net/**' },
    // Proxy with options
    '/external/**': {
      proxy: {
        to: 'https://api.example.com/**',
        // Additional H3 proxy options...
      },
    },
  }
});

Basic auth

Protect routes with HTTP Basic Authentication:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/admin/**': {
      basicAuth: {
        username: 'admin',
        password: 'supersecret',
        realm: 'Admin Area',  // Optional, shown in the browser prompt
      },
    },
    // Disable basic auth for a sub-path
    '/admin/public/**': { basicAuth: false },
  }
});

Caching (SWR / Static)

Control caching behavior with cache, swr, or static options:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    // Enable stale-while-revalidate caching
    '/blog/**': { swr: true },
    // SWR with maxAge in seconds
    '/blog/posts/**': { swr: 600 },
    // Full cache options
    '/api/data/**': {
      cache: {
        maxAge: 60,
        swr: true,
        // ...other cache options
      },
    },
    // Disable caching
    '/api/realtime/**': { cache: false },
  }
});
swr: true is a shortcut for cache: { swr: true } and swr: <number> is a shortcut for cache: { swr: true, maxAge: <number> }.

Prerender

Mark routes for prerendering at build time:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/about': { prerender: true },
    '/dynamic/**': { prerender: false },
  }
});

ISR (Vercel)

Configure Incremental Static Regeneration for Vercel deployments:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  routeRules: {
    '/isr/**': { isr: true },
    '/isr-ttl/**': { isr: 60 },
    '/isr-custom/**': {
      isr: {
        expiration: 60,
        allowQuery: ['q'],
        group: 1,
      },
    },
  }
});

Route rules reference

OptionTypeDescription
headersRecord<string, string>Custom response headers
redirectstring | { to: string, status?: number }Redirect to another URL (default status: 307)
proxystring | { to: string, ...proxyOptions }Proxy requests to another URL
corsbooleanEnable permissive CORS headers
cacheobject | falseCache options (see cache guide)
swrboolean | numberShortcut for cache: { swr: true, maxAge: number }
staticboolean | numberShortcut for static caching
basicAuth{ username, password, realm? } | falseHTTP Basic Authentication
prerenderbooleanEnable/disable prerendering
isrboolean | number | objectIncremental Static Regeneration (Vercel)

Runtime route rules

Route rules can be provided through runtimeConfig, allowing overrides via environment variables without rebuilding:

nitro.config.ts
import { defineNitroConfig } from "nitro/config";

export default defineNitroConfig({
  runtimeConfig: {
    nitro: {
      routeRules: {
        '/api/**': { headers: { 'x-env': 'production' } },
      },
    },
  },
});

Config reference

These config options control routing behavior:

OptionTypeDefaultDescription
baseURLstring"/"Base URL for all routes
apiBaseURLstring"/api"Base URL for routes in the api/ directory
apiDirstring"api"Directory name for API routes
routesDirstring"routes"Directory name for file-based routes
serverDirstring | falsefalseServer directory for scanning routes, middleware, plugins, etc.
scanDirsstring[][]Additional directories to scan for routes
routesRecord<string, string | handler>{}Route-to-handler mapping
handlersNitroEventHandler[][]Programmatic handler registration (mainly for middleware)
routeRulesRecord<string, NitroRouteConfig>{}Route rules for matching patterns
ignorestring[][]Glob patterns to ignore during file scanning