👨‍🏫 The Mastering Nuxt 3 course is now completed!

Discover the course

Server Directory

Nuxt automatically scans files inside the ~/server/api, ~/server/routes, and ~/server/middleware directories to register API and server handlers with HMR support.

Each file should export a default function defined with defineEventHandler().

The handler can directly return JSON data, a Promise or use event.node.res.end() to send response.


Create a new file in server/api/hello.ts:

export default defineEventHandler((event) => {
return {
api: 'works'

You can now universally call this API using await $fetch('/api/hello').

Server Routes

Files inside the ~/server/api are automatically prefixed with /api in their route. For adding server routes without /api prefix, you can instead put them into ~/server/routes directory.


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.

Middleware handlers should not return anything (nor close or respond to the request) and only inspect or extend the request context or throw an error.


export default defineEventHandler((event) => {
console.log('New request: ' + event.node.req.url)
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.


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.

Usage Examples

Matching 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) => `Hello, ${event.context.params.name}!`)

You can now universally call this API using await $fetch('/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

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(() => `Default foo handler`)
export default defineEventHandler(() => `Default api handler`)

Handling Requests with Body

export default defineEventHandler(async (event) => {
const body = await readBody(event)
return { body }

You can now universally call this API using $fetch('/api/submit', { method: 'post', body: { test: 123 } }).

We are using 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.

Handling Requests With Query Parameters

Sample query /api/query?param1=a&param2=b

export default defineEventHandler((event) => {
const query = getQuery(event)
return { a: query.param1, b: query.param2 }

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'

Returning other status codes

To return other status codes, you can use the setResponseStatus utility.

For example, to return 202 Accepted

export default defineEventHandler((event) => {
setResponseStatus(event, 202)

Accessing Runtime Config

export default defineEventHandler((event) => {
const config = useRuntimeConfig()
return { key: config.KEY }

Accessing Request Cookies

export default defineEventHandler((event) => {
const cookies = parseCookies(event)
return { cookies }

Advanced Usage Examples

Nitro Configuration

You can use nitro key in nuxt.config to directly set Nitro configuration.

This is an advanced option. Custom config can affect production deployments, as the configuration interface might change over time when Nitro is upgraded in semver-minor versions of Nuxt.

export default defineNuxtConfig({
// https://nitro.unjs.io/config
nitro: {}

Using a 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 (Experimental)

Note: This is an experimental feature and is only available within Node.js environments.

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((event) => {
return sendRedirect(event, '/path/redirect/to', 302)

Return a Legacy Handler or Middleware

export default fromNodeMiddleware((req, res) => {
res.end('Legacy handler')

Legacy support is possible using unjs/h3, but it is advised to avoid legacy handlers as much as you can.

export default fromNodeMiddleware((req, res, next) => {
console.log('Legacy middleware')

Never combine 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.

Example: Using Redis

export default defineNuxtConfig({
nitro: {
storage: {
'redis': {
driver: 'redis',
/* redis connector options */
port: 6379, // Redis port
host: "", // Redis host
username: "", // needs Redis >= 6
password: "",
db: 0, // Defaults to 0
tls: {} // tls/ssl

Create a new file in server/api/test.post.ts:

export default defineEventHandler(async (event) => {
const body = await readBody(event)
await useStorage().setItem('redis:test', body)
return 'Data is set'

Create a new file in server/api/test.get.ts:

export default defineEventHandler(async (event) => {
const data = await useStorage().getItem('redis:test')
return data

Create a new file in app.vue:

<div>Post state: {{ resDataSuccess }}</div>
<div>Get Data: {{ resData.text }}</div>
<script setup lang="ts">
const { data: resDataSuccess } = await useFetch('/api/test', {
method: 'post',
body: { text: 'Nuxt is Awesome!' }
const { data: resData } = await useFetch('/api/test')