Server API Routes with Nitro
Writing backend endpoints inside the same project with the server/ directory, powered by Nuxt's Nitro engine.
読了時間 2 分
So far, every example that called /api/posts was quietly relying on something worth stopping to explain: Nuxt projects can contain their own backend. The server/ directory is handled by Nitro, the server engine Nuxt is built on, and it turns files into API endpoints the same way pages/ turns files into routes.
A minimal API route
// server/api/hello.ts
export default defineEventHandler((event) => {
return { message: "Hello from the server" };
});Saving this file makes GET /api/hello return {"message": "Hello from the server"} — no router configuration, no separate Express app to run alongside Nuxt. defineEventHandler is auto-imported inside server/, and event gives you access to the request: headers, method, body, and route params.
Handling params, query strings, and the request body
// server/api/users/[id].ts
export default defineEventHandler(async (event) => {
const id = getRouterParam(event, "id");
const user = await db.users.findById(id);
if (!user) {
throw createError({ statusCode: 404, statusMessage: "User not found" });
}
return user;
});// server/api/posts.post.ts — the .post suffix restricts this file to POST requests
export default defineEventHandler(async (event) => {
const body = await readBody(event);
return await db.posts.create(body);
});The .get.ts / .post.ts / .put.ts / .delete.ts filename suffix is how one URL supports different HTTP methods across separate files, instead of branching on event.method inside a single handler.
Why this matters even for a "frontend" framework
Having a server layer inside the same project solves a real problem: a Vue SPA calling a third-party API directly from the browser means shipping any API keys to every visitor's browser. A server/api/ route runs only on the server (or at the edge, depending on deployment), so secrets stay there:
// server/api/weather.ts
export default defineEventHandler(async (event) => {
const city = getQuery(event).city;
// this API key never reaches the browser
const res = await $fetch(`https://api.weather.example/v1?key=${process.env.WEATHER_API_KEY}&city=${city}`);
return res;
});The frontend then calls your own /api/weather?city=... with useFetch, never seeing the real key or the third-party URL at all.
Nitro isn't just for /api
Nitro also supports a server/routes/ directory for non-API endpoints (like a raw redirect or a webhook receiver at a custom path) and server/middleware/ for logic that runs on every server request — logging, auth checks, setting headers — before it reaches any specific route. The /api prefix under server/api/ is a convention worth keeping, though, since it makes it immediately clear which endpoints are your app's own data API versus other server-side concerns.