Server Imports
It's possible to use different server builders with Nuxt, either Nitro 3 (nitro, with Nuxt 5+), Nitro 2 (nitropack, with Nuxt 3/4), or Vite directly.
nuxt/server exists to provide agnostic utilities for server code that aren't tied to a particular server runtime. It is the second runtime surface of Nuxt, alongside nuxt/app (also reachable as #app), which is for the part of your application that also runs in the browser.
import { defineEventHandler, getQuery } from 'nuxt/server'
export default defineEventHandler((event) => {
const { name } = getQuery<{ name?: string }>(event)
return { message: `Hello, ${name ?? 'world'}!` }
})
This code can run under @nuxt/nitro-server and under @nuxt/vite-server, and will also keep running across an h3 or Nitro major, because Nuxt absorbs those changes centrally.
nuxt/server ships from Nuxt 4.6, so one file can serve Nuxt 4.6 running nitropack v2, Nuxt 5 running Nitro v3, and anything a future builder brings.
nuxt is installed in every Nuxt application, so nuxt/server resolves with no alias and no added dependency. A module importing from it needs no peer dependency on h3 or Nitro.Types and Utilities
| Import | Purpose |
|---|---|
defineEventHandler | Define a request handler. |
createError, isNuxtError | Construct and recognise an HTTP error. |
getRequestURL, getRequestHost, getRequestProtocol | The URL, host and protocol of the request. Pass { xForwardedHost: true, xForwardedProto: true } to trust those headers, only behind a proxy you control that overwrites them. |
getRequestHeader, getRequestHeaders | Read request headers. |
getRouterParam, getRouterParams | Read the dynamic segments matched for the request. |
getRequestIP | The client IP address, with forwarded headers trusted on request. |
getQuery, getValidatedQuery | Read the query string, optionally validated. |
readBody, readValidatedBody | Read and parse the request body, optionally validated. |
getCookie, parseCookies, setCookie, deleteCookie | Read one or every cookie, and write cookies. |
setResponseStatus | Set the response status and reason phrase. |
handleCors | Apply CORS headers and answer preflight requests. |
sendRedirect | Redirect the request. |
serverFetch | Fetch a route of the app in-process, with the path relative to app.baseURL and the cookie and authorization headers forwarded unless forwardHeaders is set. It rejects on a server builder without an in-process fetch. |
useSession, getSession, updateSession, clearSession | Read and write a sealed session cookie. |
deriveSecret | A purpose-specific secret derived from appSecret. |
getRouteRules, matchRouteRules | The route rules matched for the request, or for another path. |
useRuntimeConfig | The server's runtime configuration. |
useServerHooks | The server's runtime hooks: renderer hooks such as render:html, and those declared on NuxtServerHooks. |
useAppConfig | The app config, as a copy per request when passed the event. |
Types are exported alongside the utilities: RequestEvent, RequestEventContext, NuxtRequestEvent, EventHandler, AppRouteRules, ServerRoutes, CorsOptions, ValidateResult, NuxtError, NuxtErrorDetails, NuxtErrorJSON, NuxtErrorLike, Session, SessionConfig, SessionData, SessionEvent, SessionManager, SessionPassword and SessionUpdate.
These names, except useAppConfig, are auto-imported in server code, so the import statement above is optional. Writing it keeps the file working in a project with server auto-imports off, and is what a module should do.
defineEventHandler preserves your handler's return type, which is what types $fetch and useFetch calls to the route. Annotate the value you return rather than the handler.The Event
RequestEvent is the event that nuxt/server utilities operate from, including the request, its URL, the response to be sent, and the request context.
interface RequestEvent {
readonly req: Request
url: URL
readonly res: { status?: number, statusText?: string, readonly headers: Headers }
readonly context: RequestEventContext
}
To set, read, append or remove a response header, use event.res.headers directly: it is a standard Headers object.
Nitro adds more on top, such as event.node, event.runtime and event.waitUntil(), but these four core properties work in all runtimes, and need no helpers:
export default defineEventHandler(async (event) => {
const { id } = await event.req.json()
return { id, page: event.url.searchParams.get('page') }
})
NuxtRequestEvent is the same request in the shape the configured builder provides, which is h3's H3Event under @nuxt/nitro-server.
Handlers
A handler file must default-export the result of defineEventHandler from nuxt/server. A bare function, or one wrapped by h3's defineEventHandler, receives the server runtime's own event instead.
A handler may return:
- A JSON-serializable value, sent as JSON with the status and headers set on
event.res. - A
Response, sent with its own status. Headers set onevent.res.headersare added to it, replacing a header of the same name, apart fromset-cookie, which is appended. - A
ReadableStream, streamed as the body.
Validation
readValidatedBody and getValidatedQuery take any Standard Schema (Zod, Valibot, ArkType and others), or a function that returns the validated value, true to accept the input or false to reject it.
import { defineEventHandler, readValidatedBody } from 'nuxt/server'
import { z } from 'zod'
export default defineEventHandler(async (event) => {
const user = await readValidatedBody(event, z.object({ name: z.string() }))
return { created: user.name }
})
Invalid input is rejected with a 400 whose data.issues lists what failed. Pass onError to reject it with a different error.
Client IP
getRequestIP(event) returns the address the server runtime reports for the connection, or undefined if it reports none. No forwarded header is trusted by default, because a client can send any value in one.
Behind a proxy you control, pass { xForwardedFor: true } to read the first entry of X-Forwarded-For instead. Only do this when the proxy overwrites the header: a proxy that appends to it leaves a client-sent value first.
Sessions
useSession reads the session for the request, sealing a new one into a cookie when there is none to read. The data is sealed with iron, so it lives in the cookie rather than in server storage, and the cookie defaults to httpOnly, secure, sameSite: 'lax' and path: '/'.
import { defineEventHandler, useSession } from 'nuxt/server'
export default defineEventHandler(async (event) => {
const session = await useSession<{ visits: number }>(event)
await session.update(data => ({ visits: (data.visits ?? 0) + 1 }))
return { visits: session.data.visits }
})
With no password, the session is sealed with a secret derived from the application's appSecret, so NUXT_APP_SECRET is the only thing to set. Pass a password of at least 32 characters to seal with a secret of your own instead. Set maxAge (in seconds) to expire both the cookie and the sealed value, name to change the cookie name from nuxt-session, and cookie to override any cookie attribute.
The session is unsealed once per request, so getSession may be called from as many places as you like. updateSession and clearSession are the same operations without the manager object.
Deriving Secrets
deriveSecret(purpose) resolves a secret for one purpose from appSecret: 32 bytes, hex-encoded, stable for as long as appSecret is unchanged, and distinct for every purpose. It is what the session helpers use, and what a module should use in place of asking for a secret of its own. Namespace the purpose to what owns it.
import { deriveSecret } from 'nuxt/server'
const password = await deriveSecret('my-module:tokens')
It throws a 500 naming NUXT_APP_SECRET when appSecret is unset or shorter than 32 characters.
import { useSession } from 'nitro/h3'. They are a separate implementation and are not interchangeable with these: they default to the h3 cookie name rather than nuxt-session, take their own configuration, and a session one of them issues will not unseal with the other.iron-webcrypto publishes one. When it does, existing sessions are read in the old format and resealed in the new one on their next write, so users are not signed out. Sealed values are not interchangeable with those from h3's useSession.Reaching Past the Surface
nuxt/server isn't a re-export of h3, so if you need something else that h3 and Nitro offer, such as the server lifecycle (definePlugin and defineErrorHandler) or Nitro's own runtime hooks, you would import it directly.
Most of those helpers take the event as it is:
import { defineEventHandler } from 'nuxt/server'
import { readMultipartFormData } from 'nitro/h3'
export default defineEventHandler(async (event) => {
const parts = await readMultipartFormData(event)
return { received: parts?.length ?? 0 }
})
That works for any h3 helper that only reads the request, such as readRawBody, readFormData, readMultipartFormData and assertMethod.
For a handler that needs the runtime's own event, such as one calling proxy, proxyRequest, fetchWithEvent or writeEarlyHints, or reading event.node, event.waitUntil() or event.runtime, import defineEventHandler from nitro/h3 for that handler:
import { defineEventHandler, proxyRequest } from 'nitro/h3'
export default defineEventHandler(event => proxyRequest(event, 'https://example.com'))
A file may import from both nitro/* and nuxt/server. It then runs only on Nitro v3, so a module registers it as its nitro3 variant.
Nitro's own APIs mostly take no event at all: getRouteRules(method, pathname), defineCachedHandler(), useStorage(), useDatabase() and tasks.
What Isn't Portable
These areas mean reaching outside nuxt/server, because Nuxt can't provide them for every builder:
- Storage.
useStorage()comes fromnitro/storage, and so does the driver configuration behind it. - Caching.
defineCachedHandler()anddefineCachedFunction()come fromnitro/cache. For caching that runs anywhere, depend on a cache library directly. - Server plugins.
definePlugin()comes fromnitro. Inside a plugin,useServerHooks()fromnuxt/serverregisters the renderer hooks (includingrender:html) and those declared onNuxtServerHooks; Nitro's own hooks, such asrequest,responseanderror, come fromuseNitroHooks()innitro/app. - Lazy handlers.
lazyEventHandler()comes fromnitro/h3. - Tasks.
defineTask()andrunTask()come fromnitro/task.
Server Code Only
Importing nuxt/server from a Vue component, a plugin or the shared/ directory fails the build, and the error points you at #app, #imports, $fetch and useFetch instead. Its types still resolve in those contexts, which is what lets $fetch know what your server routes return.
Modules
A module whose runtime code imports only from nuxt/server runs under any server builder from v4.6 onwards, with no version check and no dependency on h3 or Nitro:
import { defineEventHandler, useRuntimeConfig } from 'nuxt/server'
export default defineEventHandler(() => ({
version: useRuntimeConfig().myModule.version,
}))
nuxt/server external when you bundle; it will be resolved in the Nuxt build to the right server builder utilities.Supporting Nuxt versions <4.6 takes one more step, because those projects have no nuxt/server to resolve. You can instead register the portable file and the one you ship today side by side, and Nuxt will pick whichever the application can run.