Nuxt 2 is reaching End-of-Life on June 30th, 2024.


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 Hot Module Replacement (HMR) support.

Directory structure
-| 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 
) => {
return {
: 'world'
} })

You can now universally call this API in your pages and components:

<script setup lang="ts">
const { data } = await useFetch('/api/hello')

  <pre>{{ data }}</pre>

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.


export default defineEventHandler(() => 'Hello World!')

Given the example above, the /hello route will be accessible at http://localhost:3000/hello.

Note that currently server routes do not support the full functionality of dynamic routes as pages do.

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: ' + 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.


export default defineNitroPlugin((nitroApp) => {
  console.log('Nitro plugin', nitroApp)
Read more in Nitro Plugins.

Server Utilities

Server routes are powered by unjs/h3 which comes with a handy set of helpers.

Read more in Available H3 Request 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.


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

This feature is available from Nuxt >= 3.5

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"

Currently, these values won't be respected when type checking (nuxi typecheck), but you should get better type hints in your IDE.


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}!`
Alternatively, use getValidatedRouterParams with a schema validator such as Zod for runtime and type safety.

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 }
Alternatively, use readValidatedBody with a schema validator such as Zod for runtime and type safety.

You can now universally call this API using:

<script setup lang="ts">
async function submit() {
  const { body } = await $fetch('/api/submit', {
    method: 'post',
    body: { test: 123 }
We are using 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:, b: query.baz }
Alternatively, use getValidatedQuery with a schema validator such as Zod for runtime and type safety.

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( 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('', {
    headers: {
      Authorization: `token ${config.githubToken}`

  return repo
Giving the 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.

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({
  nitro: {}
Read more in Docs > Guide > Concepts > Server Engine.

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

This is an experimental feature and is available in all 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(async (event) => {
  await sendRedirect(event, '/path/redirect/to', 302)

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, or server plugins.

Example of adding a Redis storage:


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

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 {}
Read more about Nitro Storage Layer.

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(),
      port: useRuntimeConfig().redis.port,
      /* other redis connector options */

  // Mount driver
  storage.mount('redis', driver)