Nitro Server Entry
The server entry is a special handler in Nitro that acts as a global middleware, running for every incoming request before routes are matched (see request lifecycle). It's commonly used for cross-cutting concerns like authentication, logging, request preprocessing, or creating custom routing logic.
server.ts
Auto-detected
By default, Nitro automatically looks for a server.ts
(or .js
, .mjs
, .tsx
, etc.) file in your project root or scan directories.
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
}
}
import { defineHandler } from "nitro/h3";
export default defineHandler((event) => {
return { hello: "API" };
});
server.ts
is detected, Nitro will automatically log in the terminal: Using \
server.ts` as server entry.`With this setup:
/health
→ Handled by server entry/api/hello
→ Server entry runs first, then the API route/about
, etc. → Server entry runs first, then continues to routes or renderer
Framework compatibility
The server entry is a great way to integrate with other frameworks such as Elysia, Hono or H3.
import { H3 } from "h3";
const app = new H3()
app.get("/", () => "⚡️ Hello from H3!");
export default app;
import { Hono } from "hono";
const app = new Hono();
app.get("/", (c) => c.text("🔥 Hello from Hono!"));
export default app;
import { Elysia } from "elysia";
const app = new Elysia();
app.get("/", (c) => "🦊 Hello from Elysia!");
export default app;
Custom server entry file
You can specify a custom server entry file using the serverEntry
option in your Nitro configuration.
import { defineNitroConfig } from 'nitro/config'
export default defineNitroConfig({
serverEntry: './nitro.server.ts'
})
Using event handler
You can also export an event handler using defineHandler
for better type inference and access to the h3 event object:
import { defineHandler } from "nitro/h3";
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)
});
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 called as part of the global middleware stack, after route rules but before route handlers:
1. Server hook: `request`
2. Route rules (headers, redirects, etc.)
3. Global middleware (middleware/)
4. Server entry ← You are here
5. Routes (routes/)
6. Renderer (renderer.ts or index.html)
Think of the server entry as the last global middleware to run before route matching.
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)