Nitro Server Entry

Use a server entry to create a global middleware that runs for all routes before they are matched.

The server entry is a special handler in Nitro that acts as a global middleware, running for every incoming request before routes are matched. It's commonly used for cross-cutting concerns like authentication, logging, request preprocessing, or creating custom routing logic.

Auto-detected server.ts

By default, Nitro automatically looks for a server.ts (or .js, .mjs, .mts, .tsx, .jsx) file in your project root directory.

If found, Nitro will use it as the server entry and run it for all incoming requests.

export default {
  async fetch(req: Request) {
    const url = new URL(req.url);

    // Handle specific routes
    if (url.pathname === "/health") {
      return new Response("OK", {
        status: 200,
        headers: { "content-type": "text/plain" }
      });
    }

    // Add custom headers to all requests
    // Return nothing to continue to the next handler
  }
}
When server.ts is detected, Nitro will log in the terminal: Detected \server.ts` as server entry.`

With this setup:

  • /health → Handled by server entry (returns a response)
  • /api/hello → Handled by the API route handler directly
  • /about, etc. → Server entry runs first, then continues to the renderer if no response is returned

Framework compatibility

The server entry is a great way to integrate with other frameworks. Any framework that exposes a standard Web fetch(request: Request): Response interface can be used as a server entry.

Web-compatible frameworks

Frameworks that implement the Web fetch API work directly with server.ts:

server.ts
import { H3 } from "h3";

const app = new H3()

app.get("/", () => "⚡️ Hello from H3!");

export default app;

Node.js frameworks

For Node.js frameworks that use (req, res) style handlers (like Express or Fastify), name your server entry file server.node.ts instead of server.ts. Nitro will automatically detect the .node. suffix and convert the Node.js handler to a web-compatible fetch handler using srvx.

server.node.ts
import Express from "express";

const app = Express();

app.use("/", (_req, res) => {
  res.send("Hello from Express with Nitro!");
});

export default app;

Configuration

Custom server entry file

You can specify a custom server entry file using the serverEntry option in your Nitro configuration:

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

export default defineNitroConfig({
  serverEntry: "./nitro.server.ts"
})

You can also provide an object with handler and format options:

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

export default defineNitroConfig({
  serverEntry: {
    handler: "./server.ts",
    format: "node" // "web" (default) or "node"
  }
})

Handler format

The format option controls how Nitro treats the default export of your server entry:

  • "web" (default) — Expects a Web-compatible handler with a fetch(request: Request): Response method.
  • "node" — Expects a Node.js-style (req, res) handler. Nitro automatically converts it to a web-compatible handler.

When auto-detecting, the format is determined by the filename: server.node.ts uses "node" format, while server.ts uses "web" format.

Disabling server entry

Set serverEntry to false to disable auto-detection and prevent Nitro from using any server entry:

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

export default defineNitroConfig({
  serverEntry: false
})

Using event handler

You can also export an event handler using defineHandler for better type inference and access to the h3 event object:

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

export default defineHandler((event) => {
  // Add custom context
  event.context.requestId = crypto.randomUUID();
  event.context.timestamp = Date.now();

  // Log the request
  console.log(`[${event.context.requestId}] ${event.method} ${event.path}`);

  // Continue to the next handler (don't return anything)
});
If your server entry returns undefined or doesn't return anything, the request will continue to be processed by routes and the renderer. If it returns a response, the request lifecycle stops there.

Request lifecycle

The server entry is registered as a catch-all (/**) route handler. When a specific route (like /api/hello) matches a request, that route handler takes priority. For requests that don't match any specific route, the server entry runs before the renderer:

1. Server hook: `request`
2. Route rules (headers, redirects, etc.)
3. Global middleware (middleware/)
4. Route matching:
   a. Specific routes (routes/) ← if matched, handles the request
   b. Server entry ← runs for unmatched routes
   c. Renderer (renderer.ts or index.html)

When both a server entry and a renderer exist, they are chained: the server entry runs first, and if it doesn't return a response, the renderer handles the request.

Development mode

During development, Nitro watches for changes to your server entry file. When the file is created, modified, or deleted, the dev server automatically reloads to pick up the changes.

Best practices

  • Use server entry for cross-cutting concerns that affect all routes
  • Return undefined to continue processing, return a response to terminate
  • Keep server entry logic lightweight for better performance
  • Use global middleware for modular concerns instead of one large server entry
  • Consider using Nitro plugins for initialization logic
  • Avoid heavy computation in server entry (it runs for every request)
  • Don't use server entry for route-specific logic (use route handlers instead as they are more performant)