Getting Started
Install the module, define your first endpoint, and call it with types.
The Nuxt way.
import { z } from 'zod'
export default defineEndpoint({
request: { params: z.object({ id: z.coerce.number() }) },
responses: {
200: User,
404: z.object({ message: z.string() }),
},
handler: (event) => {
return findUser(event.validated.params.id) ?? event.respond(404, { message: 'Not found' })
},
})const result = await $endpoint('/api/users/:id', {
method: 'get',
params: { id: '1' },
})
if (result.status === 200) result.body.name // UserDefine the HTTP contract once, next to the handler. Request values are validated before execution, and every declared response status becomes a branchable client result with the matching body type.
export default defineEndpoint({
request: { params: z.object({ id: z.coerce.number() }) },
handler: (event) => {
return findUser(event.validated.params.id)
// client types are inferred from this return value
},
})export default defineEndpoint({
request: { params: z.object({ id: z.coerce.number() }) },
responses: { 200: User, 404: NotFound },
handler: async (event) => {
const user = await findUser(event.validated.params.id)
return user ?? event.respond(404, { message: 'Not found' })
},
})
// now the handler return is checked against the schemas,
// and client types come from the contract insteadNo big-bang migration. Only routes that export an endpoint join the contract — and inside a route, the contract can start loose and tighten when you are ready.
const result = await $endpoint('/api/users/:id', {
method: 'get',
params: { id: '123' },
})
if (result.status === 200) {
result.body.name // User
}
if (result.status === 404) {
result.body.message // typed from the 404 schema
}Declared non-2xx responses stop being unknown. Branch on the status code and the body type follows.
export default defineNuxtConfig({
modules: ['@pinia/nuxt', '@pinia/colada-nuxt', 'nuxt-endpoints'],
})import { useQuery } from '@pinia/colada'
import { queryOptions } from '#endpoints/colada'
const route = useRoute()
const request = $endpoint('/api/users/:id', {
method: 'get',
params: { id: String(route.params.id) },
})
const user = useQuery(queryOptions(request))
if (user.data.value?.status === 200) {
user.data.value.body.name // User
}Pinia Colada integration
The same $endpoint request object plugs directly into Pinia Colada. Colada owns server-state behavior while Nuxt Endpoints keeps request identity, HTTP idempotency, and status-aware response types aligned with the server contract.
{
"openapi": "3.1.0",
"paths": {
"/api/users/{id}": {
"get": {
"operationId": "getApiUsersById",
"parameters": [
{ "name": "id", "in": "path", "required": true }
],
"responses": {
"200": { "description": "OK" },
"404": { "description": "Not found" }
}
}
}
}
}The OpenAPI 3.1 document is generated from the same contracts that run your validation, so there is no spec to keep in sync. Endpoints stay plain HTTP routes.
Install the module, define your first endpoint, and call it with types.
Follow the public Nuxt 5 integration branches and see what is implemented today.
Declare validated request and response contracts with the canonical route-handler API.
Call server routes by typed path and method.
Work with status-aware endpoint results or raw Web Responses.
Use typed endpoint request objects as Pinia Colada queries and mutations with its official Nuxt integration.
Serve OpenAPI 3.1 from endpoint definitions.
Zod, Valibot, and Effect Schema are supported without locking the API layer to one vendor.
Idempotency-Key replay protection for unsafe endpoints, with an application-owned durable storage contract.
Handle files, streams, redirects, proxies, raw Responses, and 204 routes.
Convert one route at a time. Every other route keeps working unchanged.
One route definition powers the server, client, and documentation.
The drift problem in plain Nuxt apps, and the single-contract idea behind the module.
How Nuxt Endpoints relates to Nuxt typed fetch, tRPC, and OpenAPI tooling.
Supported platform versions, current constraints, and upstream integration plans.