Nitro Server Entry

Use a server entry to handle every request that no route matched.

The server entry is a special handler that Nitro registers as a catch-all (/**) route. Specific routes always win, so the server entry only runs for requests none of them matched, right before the renderer. It is commonly used to mount another framework inside Nitro, or to implement custom routing for everything the filesystem routes don't claim.

Warning

The server entry is a fallback, not a global middleware: it does not run for requests a route handled. For cross-cutting concerns that must apply to every request (authentication, logging, request preprocessing), use middleware instead.

#Auto-detected server.ts

By default, Nitro automatically looks for a server.ts (or .js, .mjs, .mts, .tsx, .jsx) file in your serverDir (if set) or 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
  }
}

Tip

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;
server.ts
import { Hono } from "hono";

const app = new Hono();

app.get("/", (c) => c.text("🔥 Hello from Hono!"));

export default app;
server.ts
import { Elysia } from "elysia";

const app = new Elysia();

app.get("/", () => "🦊 Hello from Elysia!");

export default app.compile();

#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;
server.node.ts
import Fastify from "fastify";

const app = Fastify();

app.get("/", () => "Hello, Fastify with Nitro!");

await app.ready();

export default app.routing;

#Server options

When the server entry's default export is a plain object, every property other than fetch is passed as-is to the srvx server started by the Node.js, Bun and Deno presets. This gives you control over the server itself: middleware and plugins that run for every request (before Nitro), tls, maxRequestBodySize, trustProxy, gracefulShutdown, runtime specific settings (node, bun, deno), and so on.

Use the defineServerEntry helper for typed options:

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

export default defineServerEntry({
  fetch(req) {
    return new Response("Hello from server entry!");
  },
  port: 8080,
  maxRequestBodySize: 1024 * 1024,
  middleware: [
    async (req, next) => {
      const res = await next();
      res.headers.set("x-powered-by", "nitro");
      return res;
    },
  ],
});
Read more in srvx server options.

Note

  • NITRO_PORT/PORT, NITRO_HOST/HOST and NITRO_SSL_CERT/NITRO_SSL_KEY environment variables take precedence over the port, hostname and tls options, so the server stays configurable at runtime.
  • During development (nitro dev), options are applied to the dev worker server, except listener options (port, hostname, protocol, tls, silent, gracefulShutdown) which are controlled by the dev server and CLI (--port, --host). The Vite dev server applies none of them.
  • manual is not supported: presets start listening immediately.
  • Options are only read from plain object exports (not from framework instances like export default app) and never from Node.js format entries (server.node.ts).

#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 { defineConfig } from "nitro";

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

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

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

export default defineConfig({
  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 { defineConfig } from "nitro";

export default defineConfig({
  serverEntry: false
})

#Using an event handler

Instead of a Web fetch handler, you can export an event handler made with defineHandler for better type inference and access to the H3 event object:

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

export default defineHandler((event) => {
  // Runs only for requests no route matched
  if (event.url.pathname.startsWith("/api/")) {
    throw new HTTPError("Unknown API endpoint", { status: 404 });
  }

  // Add context for the renderer
  event.context.requestId = crypto.randomUUID();

  // Return nothing to hand the request over to the renderer
});

Important

Returning undefined (or nothing) hands the request to the renderer. Without a renderer, Nitro answers with an empty 200 response. Returning a value ends the request there.

#Startup logic

The server entry is imported eagerly, so its top-level code runs once when the server starts, not on the first request.

Keep in mind:

  • Top-level code runs before runtime plugins, so hooks registered by plugins are not available yet. Do not call useNitroApp() at the top level; call it inside the handler instead.
  • A top-level await delays startup: the server does not accept requests until the server entry has finished evaluating.
  • On serverless and edge presets, "startup" means every cold start of a new instance. Some runtimes, like Cloudflare Workers, don't allow I/O (such as fetch) in the global scope.
  • In development, top-level code runs again whenever the server entry is reloaded.

#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 (static assets first, then middleware/)
4. Route-scoped middleware (handlers config)
5. 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.

Read more in Lifecycle.

#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 the server entry as the fallback for requests no route matched, or to mount another framework
  • Use middleware for concerns that must apply to every request, matched routes included
  • Return undefined to continue to the renderer; return a value to end the request
  • Keep the server entry lightweight; it runs for every unmatched request
  • Use runtime plugins for one-time initialization logic
  • Don't use the server entry for route-specific logic; route handlers are more performant

Nitro  builds full-stack servers that deploy anywhere.