# 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](https://h3.dev/guide/api/h3event#h3event-properties) object.

<code-group>

```ts [Single function]
import type { H3Event } from "nitro";

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

```ts [defineHandler]
import { defineHandler } from "nitro";

// For better type inference
export default defineHandler((event) => {
  return "world";
});
```
</code-group>

## Filesystem routing

Nitro supports file-based routing for your API routes (files are automatically mapped to [h3 routes](https://h3.dev/guide/basics/routing)). 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](#specific-request-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.

```txt
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:

```txt
routes/
  api/
    (admin)/
      users.ts   <-- /api/users
      reports.ts <-- /api/reports
    (public)/
      index.ts   <-- /api
package.json
```

> [!NOTE] 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:

```ts [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`](https://h3.dev/utils/request#getrouterparamevent-name-opts-decode) utility.

```ts [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:

```txt [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.

```ts [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.

```ts [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:

```txt [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`.

<code-group>

```js [GET]
// 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!`;
});
```

```js [POST]
// routes/users.post.ts
import { defineHandler } from "nitro";

export default defineHandler(async (event) => {
  const body = await event.req.json();

  // Do something with body like saving it to a database

  return { updated: true };
});
```
</code-group>

### 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`.

```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):

```txt
routes/
  env/
    index.dev.ts       <-- /env (dev only)
    index.get.prod.ts  <-- /env (GET, prod only)
```

> [!TIP]
> You can specify multiple environments or specify a preset name as environment using programmatic registration of routes via [`routes`](#routes-config) 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.

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

| Option | Type | Description |
| --- | --- | --- |
| `handler` | `string` | Path to event handler file or virtual module ID |
| `method` | `string` | HTTP method to match (`get`, `post`, etc.) |
| `lazy` | `boolean` | Use lazy loading to import handler |
| `format` | `"web" \| "node"` | Handler type. `"node"` handlers are converted to web-compatible |
| `env` | `string \| 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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

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

Each handler entry supports the following options:

| Option | Type | Description |
| --- | --- | --- |
| `route` | `string` | HTTP pathname pattern (e.g., `/test`, `/api/:id`, `/blog/**`) |
| `handler` | `string` | Path to event handler file or virtual module ID |
| `method` | `string` | HTTP method to match (`get`, `post`, etc.) |
| `middleware` | `boolean` | Run handler as middleware before route handlers |
| `lazy` | `boolean` | Use lazy loading to import handler |
| `format` | `"web" \| "node"` | Handler type. `"node"` handlers are converted to web-compatible |
| `env` | `string \| string[]` | Environments to include this handler (`"dev"`, `"prod"`, `"prerender"`, or a preset name) |

## Middleware

Nitro route middleware can hook into the request lifecycle.

<tip>

A middleware can modify the request before it is processed, not after.
</tip>

Middleware are auto-registered within the `middleware/` directory.

```md
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.

```ts [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](https://h3.dev/guide/basics/handler#object-syntax).

<note>

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!**
</note>

### Route Meta

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

> [!IMPORTANT]
> This feature is currently experimental.

```ts [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");
```

<read-more>

This feature is currently usable to specify OpenAPI meta. See swagger specification for available OpenAPI options.
</read-more>

### Execution order

Middleware are executed in directory listing order.

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

Prefix middleware with a number to control their execution order.

```md
middleware/
  1.logger.ts <-- First
  2.auth.ts <-- Second
  3.... <-- Third
```

<note>

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.
</note>

### 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:

```ts [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`](#handlers-config) config with the `middleware` option and a specific `route`:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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](https://h3.dev/guide/basics/error) 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`](/config#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](https://github.com/h3js/rou3)) to route options.

When `cache` option is set, handlers matching pattern will be automatically wrapped with `defineCachedHandler`. See the [cache guide](/docs/cache) to learn more about this function.

<note>

`swr: true|number` is shortcut for `cache: { swr: true, maxAge: number }`
</note>

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

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  routeRules: {
    '/blog/**': { swr: true },
    '/blog2/**': { swr: 600 },
    '/blog3/**': { static: true },
    '/blog4/**': { 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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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`:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

export default defineConfig({
  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 },
  }
});
```

<tip>

`swr: true` is a shortcut for `cache: { swr: true }` and `swr: <number>` is a shortcut for `cache: { swr: true, maxAge: <number> }`.
</tip>

### Prerender

Mark routes for prerendering at build time:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

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

### ISR (Vercel)

Configure Incremental Static Regeneration for Vercel deployments:

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

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

### Route rules reference

| Option | Type | Description |
| --- | --- | --- |
| `headers` | `Record<string, string>` | Custom response headers |
| `redirect` | `string \| { to: string, status?: number }` | Redirect to another URL (default status: `307`) |
| `proxy` | `string \| { to: string, ...proxyOptions }` | Proxy requests to another URL |
| `cors` | `boolean` | Enable permissive CORS headers |
| `cache` | `object \| false` | Cache options (see [cache guide](/docs/cache)) |
| `swr` | `boolean \| number` | Shortcut for `cache: { swr: true, maxAge: number }` |
| `static` | `boolean \| number` | Shortcut for static caching |
| `basicAuth` | `{ username, password, realm? } \| false` | HTTP Basic Authentication |
| `prerender` | `boolean` | Enable/disable prerendering |
| `isr` | `boolean \| number \| object` | Incremental Static Regeneration (Vercel) |

### Runtime route rules

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

```ts [nitro.config.ts]
import { defineConfig } from "nitro";

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

## Config reference

These config options control routing behavior:

| Option | Type | Default | Description |
| --- | --- | --- | --- |
| `baseURL` | `string` | `"/"` | Base URL for all routes |
| `apiBaseURL` | `string` | `"/api"` | Base URL for routes in the `api/` directory |
| `apiDir` | `string` | `"api"` | Directory name for API routes |
| `routesDir` | `string` | `"routes"` | Directory name for file-based routes |
| `serverDir` | `string \| false` | `false` | Server directory for scanning routes, middleware, plugins, etc. |
| `scanDirs` | `string[]` | `[]` | Additional directories to scan for routes |
| `routes` | `Record<string, string \| handler>` | `{}` | Route-to-handler mapping |
| `handlers` | `NitroEventHandler[]` | `[]` | Programmatic handler registration (mainly for middleware) |
| `routeRules` | `Record<string, NitroRouteConfig>` | `{}` | Route rules for matching patterns |
| `ignore` | `string[]` | `[]` | Glob patterns to ignore during file scanning |
