Suggest an editImprove this articleRefine the answer for “How to create API routes in Nuxt?”. Your changes go to moderation before they’re published.Approval requiredContentWhat you’re changing🇺🇸EN🇺🇦UAPreviewTitle (EN)Short answer (EN)**Nuxt API routes** are server-side endpoints in `server/api/` that map automatically to `/api/*` URLs. Wrap the handler in `defineEventHandler` and Nuxt handles routing. ```typescript // server/api/hello.get.ts export default defineEventHandler(() => ({ message: 'hello' })) // GET /api/hello → { message: 'hello' } ``` **Key:** file path equals URL path, no router config needed.Shown above the full answer for quick recall.Answer (EN)Image**Nuxt API routes** are server-side endpoints that live in `server/api/` and map automatically to `/api/*` URLs. No separate server, no router config. The file path is the route. ## Theory ### TL;DR - Drop a `.ts` file in `server/api/` and Nuxt registers it as an endpoint automatically - Wrap every handler with `defineEventHandler` (required by H3, Nuxt's server engine) - Add `.get.ts` or `.post.ts` to the filename to restrict a route to one HTTP method - Read request data with `getQuery(event)`, `readBody(event)`, and `getRouterParam(event, 'name')` - Throw errors through `createError({ statusCode: 404, message: 'Not found' })` ### Quick example ```typescript // server/api/users/[id].get.ts export default defineEventHandler(async (event) => { const id = getRouterParam(event, 'id') if (!id) { throw createError({ statusCode: 400, message: 'Missing id' }) } return { id, name: 'Alice' } }) // GET /api/users/42 → { id: '42', name: 'Alice' } // POST /api/users/42 → 405 Method Not Allowed (automatic) ``` The `.get.ts` suffix locks this file to GET requests. Any other method returns 405 without any extra code from you. ### File naming and URL mapping Nuxt reads the file path and builds the URL directly: - `server/api/users.ts` → `/api/users` - `server/api/users/index.ts` → `/api/users` - `server/api/users/[id].ts` → `/api/users/:id` (all methods) - `server/api/users/[id].get.ts` → `GET /api/users/:id` - `server/api/users/[id].delete.ts` → `DELETE /api/users/:id` Bracket syntax `[id]` marks a dynamic segment. Multiple params work in a single path too: `[userId]/posts/[postId].ts`. ### Reading request data Three helpers cover most cases: ```typescript // Query string: GET /api/search?q=nuxt&limit=10 const query = getQuery(event) // { q: 'nuxt', limit: '10' } // Request body: POST /api/users const body = await readBody(event) // { name: 'Bob', email: '...' } // Route param: GET /api/users/42 const id = getRouterParam(event, 'id') // '42' ``` `readBody` is async. Forgetting `await` returns a Promise object, not your data. That is the most common bug in new Nuxt projects. ### Where server routes run Server routes execute in Nitro, Nuxt's server engine built on H3. This is a Node.js environment, not a browser. You have access to `process.env`, `useRuntimeConfig()`, and Nitro storage. You do not have `window`, `document`, or any Vue reactivity. Mixing those up causes failures that are painful to debug. ### Common mistakes **1. Skipping `await` on `readBody`** ```typescript const body = readBody(event) // Bug: this is a Promise const body = await readBody(event) // Correct ``` **2. Manual method check instead of per-method files** ```typescript // Works, but not recommended export default defineEventHandler(async (event) => { if (event.method === 'GET') { ... } if (event.method === 'POST') { ... } }) ``` Better: create `users.get.ts` and `users.post.ts`. Each file is smaller, easier to test, and TypeScript gets accurate types per method. **3. Throwing a raw Error** ```typescript throw new Error('Not found') // Wrong: status 500, leaks stack trace throw createError({ statusCode: 404, message: 'Not found' }) // Correct ``` **4. Using `event.context.params` instead of `getRouterParam`** ```typescript const id = event.context.params?.id // Works, not the recommended H3 API const id = getRouterParam(event, 'id') // Preferred ``` Both work today. `getRouterParam` handles edge cases in nested routes more reliably. ### Calling routes from Nuxt pages From any component or page, use `useFetch` or `$fetch`: ```typescript // pages/users/[id].vue const { data } = await useFetch(`/api/users/${id}`) ``` `useFetch` is SSR-aware. On the server it calls the handler in-process, skipping the HTTP layer entirely. On the client it makes a real HTTP request. I've seen this behavior confuse developers who profiled the app and expected a network call in both environments. ### Follow-up questions **Q:** What is the difference between `server/api/` and `server/routes/`? **A:** Files in `server/api/` get the `/api` prefix automatically. Files in `server/routes/` map to any URL without that prefix. Use `server/routes/` for webhooks, `/sitemap.xml`, or any endpoint that should not live under `/api`. **Q:** How do you share logic between multiple API routes? **A:** Extract it into a function in `server/utils/`. Nitro auto-imports everything from that folder, so you can call the function in any handler without an explicit import. **Q:** Can you protect API routes with authentication middleware? **A:** Yes. Create a file in `server/middleware/` that validates the token and sets `event.context.user`. Each route handler then reads `event.context.user` without repeating the auth check. **Q:** How do you return a custom HTTP status code like 201? **A:** Call `setResponseStatus(event, 201)` before returning. For errors, pass the status code directly to `createError({ statusCode: 422, message: '...' })`. ## Examples ### Minimal GET endpoint ```typescript // server/api/ping.get.ts export default defineEventHandler(() => { return { status: 'ok', timestamp: Date.now() } }) // GET /api/ping → { status: 'ok', timestamp: 1720000000000 } ``` No `async` here because there is no I/O. The returned object is serialized to JSON automatically by Nitro. ### POST endpoint with body validation ```typescript // server/api/users.post.ts export default defineEventHandler(async (event) => { const body = await readBody<{ name: string; email: string }>(event) if (!body.name || !body.email) { throw createError({ statusCode: 422, message: 'name and email are required', }) } const user = await db.users.create({ data: body }) return user }) ``` The generic on `readBody<T>` tells TypeScript the shape of `body`, so `body.name` and `body.email` get autocomplete and type checking throughout the handler.For the reviewerNote to the moderator (optional)Visible only to the moderator. Helps review go faster.