Lifecycle

Understand how Nitro runs and serves incoming requests to your application.

Request lifecycle

A request can be intercepted and terminated (with or without a response) from any of these layers, in this order:

request hook

The request hook is the first code that runs for every incoming request. It is registered via a runtime plugin:

plugins/request-hook.ts
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("request", (event) => {
    console.log(`Incoming request on ${event.req.url}`);
  });
});
Errors thrown inside the request hook are captured by the error hook and do not terminate the request pipeline.

Route rules

Matching route rules from the Nitro config execute next. Route rules run as middleware, before any global middleware, and most of them alter the response without terminating it (for instance, adding a header or setting a cache policy).

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  routeRules: {
    '/**': { headers: { 'x-nitro': 'first' } }
  }
})
Read more in Routing > Route rules.

Static assets

When static asset serving is enabled (the default for most presets), Nitro registers a static assets handler as the first global middleware. It checks if the request matches a file in the public/ directory before other global middleware and route handlers run.

If a match is found, the static file is served immediately with appropriate Content-Type, ETag, Last-Modified, and Cache-Control headers. The request is terminated and no further middleware or routes are executed.

Static assets also support content negotiation for pre-compressed files (gzip, brotli, zstd) via the Accept-Encoding header.

Global middleware

Any global middleware defined in the middleware/ directory runs next:

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

export default defineHandler((event) => {
  event.context.info = { name: "Nitro" };
});
Returning from a middleware will close the request and should be avoided when possible.
Learn more about Nitro middleware.

Routed middleware

Middleware registered for a specific route pattern (via the handlers config) runs after global middleware. Any middleware attached to the matched route itself runs last, right before the route handler.

Routes

Nitro matches the incoming request against the routes defined in the routes/ folder.

routes/api/hello.ts
export default (event) => ({ world: true })
Learn more about Nitro filesystem routing.

Server entry

If a server entry is defined, it catches all requests not matched by any route, acting as a /** route handler.

server.ts
import { defineHandler } from "nitro";

export default defineHandler((event) => {
  if (event.url.pathname === "/") {
    return "Home page";
  }
});
Learn more about the Nitro server entry.

Renderer

If no route is matched, Nitro will look for a renderer handler (defined or auto-detected) to handle the request.

Learn more about Nitro renderer.

response hook

After the response is created (from any of the layers above), the response hook runs. This hook receives the final Response object and the event, and can be used to inspect or modify response headers:

plugins/response-hook.ts
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("response", (res, event) => {
    console.log(`Response ${res.status} for ${event.req.url}`);
  });
});
The response hook runs for every response, including static assets, middleware-terminated requests, and error responses.

Error handling

When an error occurs at any point in the request lifecycle, Nitro:

Calls the error hook with the error and context (including the event and source tags).

Passes the error to the error handler which converts it into an HTTP response.

Throwing errors

Throw an HTTPError from any handler or middleware to end the request with a specific status code, message, and optional data:

routes/signup.ts
import { defineHandler, HTTPError } from "nitro";

export default defineHandler((event) => {
  // Message and details
  throw new HTTPError("Invalid user input", { status: 400 });

  // Status code shortcut
  throw HTTPError.status(400, "Bad Request");

  // Full object form
  throw new HTTPError({
    status: 400,
    message: "Invalid user input",
    data: { field: "email" },
  });
});
Any other thrown value (such as a plain new Error()) is treated as an unhandled error: the response is always 500 and the message, data, and stack are hidden from the client for security.

Default error responses

In production, the default error handler always responds with JSON:

{
  "error": true,
  "status": 400,
  "message": "Invalid user input",
  "data": { "field": "email" }
}

In development, programmatic requests receive the same JSON format, but when the request's Accept header includes text/html (browsers), Nitro renders an interactive HTML error page with a source-mapped stack trace.

Custom error handlers

To control error response formatting, set errorHandler in the Nitro config to a file that exports a handler with defineErrorHandler:

import { defineConfig } from "nitro";

export default defineConfig({
  errorHandler: "~/error",
});

Return a Response to send it to the client, or return nothing to fall through to the next handler. errorHandler also accepts an array of handlers that run in order — the first one that returns a response wins, and the built-in default handler is always appended as the last entry, so there is always a fallback response:

nitro.config.ts
import { defineConfig } from "nitro";

export default defineConfig({
  errorHandler: ["~/error/known-errors", "~/error/fallback"],
});

If a handler itself throws, the failure is logged and the chain continues with the next handler. Each handler also receives the built-in defaultHandler as { defaultHandler } in its third argument, so you can reuse the default rendering within your own logic.

See a full custom error handler example.

To customize error rendering for the development server only (without affecting the production bundle), pass a handler function as devErrorHandler:

nitro.config.ts
import { defineConfig } from "nitro";
import errorHandler from "./error.ts";

export default defineConfig({
  devErrorHandler: errorHandler,
});

Observing errors

To observe errors without changing the response, use the error runtime hook from a plugin:

plugins/errors.ts
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("error", (error, context) => {
    console.error("Captured error:", error);
    // context.event - the H3 event (if available)
    // context.tags  - error source tags like "request", "response", "plugin"
  });
});

You can also feed errors into the same pipeline programmatically with nitroApp.captureError. Errors are also tracked per-request in event.req.context.nitro.errors for inspection in later hooks.

Additionally, unhandled promise rejections and uncaught exceptions at the process level are automatically captured into the error hook with the tags "unhandledRejection" and "uncaughtException".

Server shutdown

When the Nitro server is shutting down, the close hook is called. Use this to clean up resources such as database connections, timers, or external service handles:

plugins/cleanup.ts
import { definePlugin } from "nitro";

export default definePlugin((nitroApp) => {
  nitroApp.hooks.hook("close", async () => {
    // Clean up resources
  });
});

Hooks reference

All runtime hooks are registered through runtime plugins using nitroApp.hooks.hook().

HookSignatureWhen it runs
request(event: HTTPEvent) => void | Promise<void>Start of each request, before routing.
response(res: Response, event: HTTPEvent) => void | Promise<void>After the response is created, before it is sent.
error(error: Error, context: { event?, tags? }) => voidWhen any error is captured during the lifecycle.
close() => voidWhen the Nitro server is shutting down.
The NitroRuntimeHooks interface is augmentable. Deployment presets (such as Cloudflare) can extend it with platform-specific hooks.
Learn more about Nitro plugins and hook usage examples.