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:
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.
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:
import { defineConfig } from "nitro";
export default defineConfig({
serverEntry: "./nitro.server.ts"
})You can also provide an object with handler and format options:
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 afetch(request: Request): Responsemethod."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:
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:
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.
#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.
#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
undefinedto 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