Docs Guide Directory Structure
server
The server/ directory is used to register API and server handlers to your application.
Nuxt automatically scans files inside these directories to register API and server handlers with HMR support.
-| server/---| api/-----| hello.ts # /api/hello---| routes/-----| bonjour.ts # /bonjour---| middleware/-----| log.ts # log all requests
Each file should export a default function defined with defineEventHandler()
or eventHandler()
(alias).
The handler can directly return JSON data, a Promise
, or use event.node.res.end()
to send a response.
export default defineEventHandler((event) => { return { hello: 'world' }})
You can now universally call this API in your pages and components:
<script setup lang="ts">const { data } = await useFetch('/api/hello')</script><template> <pre>{{ data }}</pre></template>
Server Routes
Files inside the ~/server/api
are automatically prefixed with /api
in their route.
To add server routes without /api
prefix, put them into ~/server/routes
directory.
Example:
export default defineEventHandler(() => 'Hello World!')
Given the example above, the /hello
route will be accessible at http://localhost:3000/hello.
Server Middleware
Nuxt will automatically read in any file in the ~/server/middleware
to create server middleware for your project.
Middleware handlers will run on every request before any other server route to add or check headers, log requests, or extend the event's request object.
Examples:
export default defineEventHandler((event) => { console.log('New request: ' + getRequestURL(event))})
export default defineEventHandler((event) => { event.context.auth = { user: 123 }})
Server Plugins
Nuxt will automatically read any files in the ~/server/plugins
directory and register them as Nitro plugins. This allows extending Nitro's runtime behavior and hooking into lifecycle events.
Example:
export default defineNitroPlugin((nitroApp) => { console.log('Nitro plugin', nitroApp)})
Server Utilities
Server routes are powered by unjs/h3 which comes with a handy set of helpers.
You can add more helpers yourself inside the ~/server/utils
directory.
For example, you can define a custom handler utility that wraps the original handler and performs additional operations before returning the final response.
Example:
import type { EventHandler, EventHandlerRequest } from 'h3'export const defineWrappedResponseHandler = <T extends EventHandlerRequest, D> ( handler: EventHandler<T, D>): EventHandler<T, D> => defineEventHandler<T>(async event => { try { // do something before the route handler const response = await handler(event) // do something after the route handler return { response } } catch (err) { // Error handling return { err } } })
Server Types
To improve clarity within your IDE between the auto-imports from 'nitro' and 'vue', you can add a ~/server/tsconfig.json
with the following content:
{ "extends": "../.nuxt/tsconfig.server.json"}
Although right now these values won't be respected when type checking (nuxi typecheck
), you should get better type hints in your IDE.
Recipes
Route Parameters
Server routes can use dynamic parameters within brackets in the file name like /api/hello/[name].ts
and be accessed via event.context.params
.
export default defineEventHandler((event) => { const name = getRouterParam(event, 'name') return `Hello, ${name}!`})
You can now universally call this API on /api/hello/nuxt
and get Hello, nuxt!
.
Matching HTTP Method
Handle file names can be suffixed with .get
, .post
, .put
, .delete
, ... to match request's HTTP Method.
export default defineEventHandler(() => 'Test get handler')
export default defineEventHandler(() => 'Test post handler')
Given the example above, fetching /test
with:
- GET method: Returns
Test get handler
- POST method: Returns
Test post handler
- Any other method: Returns 405 error
You can also use index.[method].ts
inside a directory for structuring your code differently, this is useful to create API namespaces.
export default defineEventHandler((event) => { // handle GET requests for the `api/foo` endpoint})
Catch-all Route
Catch-all routes are helpful for fallback route handling.
For example, creating a file named ~/server/api/foo/[...].ts
will register a catch-all route for all requests that do not match any route handler, such as /api/foo/bar/baz
.
export default defineEventHandler((event) => { // event.context.path to get the route path: '/api/foo/bar/baz' // event.context.params._ to get the route segment: 'bar/baz' return `Default foo handler`})
You can set a name for the catch-all route by using ~/server/api/foo/[...slug].ts
and access it via event.context.params.slug
.
export default defineEventHandler((event) => { // event.context.params.slug to get the route segment: 'bar/baz' return `Default foo handler`})
Body Handling
export default defineEventHandler(async (event) => { const body = await readBody(event) return { body }})
You can now universally call this API using:
<script setup>async function submit() { const { body } = await $fetch('/api/submit', { method: 'post', body: { test: 123 } })}</script>
submit.post.ts
in the filename only to match requests with POST
method that can accept the request body. When using readBody
within a GET request, readBody
will throw a 405 Method Not Allowed
HTTP error.Query Parameters
Sample query /api/query?foo=bar&baz=qux
export default defineEventHandler((event) => { const query = getQuery(event) return { a: query.foo, b: query.baz }})
Error Handling
If no errors are thrown, a status code of 200 OK
will be returned.
Any uncaught errors will return a 500 Internal Server Error
HTTP Error.
To return other error codes, throw an exception with createError
:
export default defineEventHandler((event) => { const id = parseInt(event.context.params.id) as number if (!Number.isInteger(id)) { throw createError({ statusCode: 400, statusMessage: 'ID should be an integer', }) } return 'All good'})
Status Codes
To return other status codes, use the setResponseStatus
utility.
For example, to return 202 Accepted
export default defineEventHandler((event) => { setResponseStatus(event, 202)})
Runtime Config
export default defineEventHandler(async (event) => { const config = useRuntimeConfig(event) const repo = await $fetch('https://api.github.com/repos/nuxt/nuxt', { headers: { Authorization: `token ${config.githubToken}` } }) return repo})
event
as argument to useRuntimeConfig
is optional, but it is recommended to pass it to get the runtime config overwritten by environment variables at runtime for server routes.Request Cookies
export default defineEventHandler((event) => { const cookies = parseCookies(event) return { cookies }})
Advanced Usage
Nitro Config
You can use nitro
key in nuxt.config
to directly set Nitro configuration.
export default defineNuxtConfig({ // https://nitro.unjs.io/config nitro: {}})
Nested Router
import { createRouter, defineEventHandler, useBase } from 'h3'const router = createRouter()router.get('/test', defineEventHandler(() => 'Hello World'))export default useBase('/api/hello', router.handler)
Sending Streams
import fs from 'node:fs'import { sendStream } from 'h3'export default defineEventHandler((event) => { return sendStream(event, fs.createReadStream('/path/to/file'))})
Sending Redirect
export default defineEventHandler(async (event) => { await sendRedirect(event, '/path/redirect/to', 302)})
Legacy Handler or Middleware
export default fromNodeMiddleware((req, res) => { res.end('Legacy handler')})
export default fromNodeMiddleware((req, res, next) => { console.log('Legacy middleware') next()})
next()
callback with a legacy middleware that is async
or returns a Promise
.Server Storage
Nitro provides a cross-platform storage layer. In order to configure additional storage mount points, you can use nitro.storage
, or server plugins.
Example of adding a Redis storage:
Using nitro.storage
:
export default defineNuxtConfig({ nitro: { storage: { redis: { driver: 'redis', /* redis connector options */ port: 6379, // Redis port host: "127.0.0.1", // Redis host username: "", // needs Redis >= 6 password: "", db: 0, // Defaults to 0 tls: {} // tls/ssl } } }})
Then in your API handler:
export default defineEventHandler(async (event) => { // List all keys with const keys = await useStorage('redis').getKeys() // Set a key with await useStorage('redis').setItem('foo', 'bar') // Remove a key with await useStorage('redis').removeItem('foo') return {}})
Alternatively, you can create a storage mount point using a server plugin and runtime config:
import redisDriver from 'unstorage/drivers/redis'export default defineNitroPlugin(() => { const storage = useStorage() // Dynamically pass in credentials from runtime configuration, or other sources const driver = redisDriver({ base: 'redis', host: useRuntimeConfig().redis.host, port: useRuntimeConfig().redis.port, /* other redis connector options */ }) // Mount driver storage.mount('redis', driver)})