Define once.
Validate at runtime.
Type-safe end-to-end.

Tech stack
  • Nuxt
  • Zod
  • Valibot
  • Effect
  • Vue Query
  • OpenAPI

Supports Nuxt 4.5+ — see compatibility

Define params with your schema library.

Choose Zod, Valibot, or Effect Schema for request params, then reuse that schema for types, clients, and OpenAPI.

Serverserver/api/users/[id].get.ts
export const endpoint = defineEndpoint({
params: z.object({ id: z.coerce.number() }), // '123' → 123
})
export const endpoint = defineEndpoint({
params: z.object({ id: z.coerce.number() }), // '123' → 123
})
server/api/users/[id].get.ts
import { z } from 'zod'

export const endpoint = defineEndpoint({
  params: z.object({ id: z.coerce.number() }),
  responses: {
    200: User,
    404: z.object({ message: z.string() }),
  },
})
app code — nothing to import
const user = await $endpoint('/api/users/:id', {
  method: 'get',
  params: { id: '1' },
})

user.name // User — inferred from the contract

One contract, everything typed.

Define the HTTP contract once, next to the handler, in the schema library you already use. Runtime validation, the generated client, and OpenAPI all derive from that single definition — no codegen step to run.

  • params, query, headers, and body are validated before your handler runs
  • $endpoint and useEndpoint are fully inferred — auto-imported, routes stay ordinary server/api files
  • Standard Schema based: Zod v4, Valibot, and Effect Schema
Define your first endpoint
step 1 — ship it without a schema
export default defineEndpoint({
  params: z.object({ id: z.coerce.number() }),
  handler: ({ params }) => {
    return findUser(params.id)
    // client types are inferred from this return value
  },
})
step 2 — tighten the contract
export const endpoint = defineEndpoint({
  params: z.object({ id: z.coerce.number() }),
  responses: { 200: User, 404: NotFound },
})

// now the handler return is checked against the schemas,
// and client types come from the contract instead

Incremental by design.

No 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.

  • Opt-in per route: everything else stays a plain Nitro route
  • No response schema yet? Client types are inferred from your handler return value
  • Declare responses to lock the handler in — or delete the export to roll back
Read the adoption guide
status-typed result
const result = await $endpoint('/api/users/:id', {
  method: 'get',
  params: { id: '123' },
}).result()

if (result.status === 200) {
  result.body.name // User
}

if (result.status === 404) {
  result.body.message // typed from the 404 schema
}

Errors are typed too.

Declared non-2xx responses stop being unknown. Branch on the status code and the body type follows.

  • responses: { 200: User, 404: NotFound } — TypeScript checks handler returns
  • .result() narrows the response body by status
  • .raw() returns the native Web Response for streaming or low-level access
Responses
nuxt.config.ts — optional SSR setup
export default defineNuxtConfig({
  modules: ['nuxt-endpoints'],
  endpoints: {
    client: {
      query: { setup: 'auto' },
    },
  },
})
pages/users/[id].vue
import { useQuery } from '@tanstack/vue-query'
import { endpointQueryOptions } from '#endpoints/query'

const route = useRoute()
const user = useQuery(
  endpointQueryOptions.getUser(() => ({
    params: { id: String(route.params.id) },
  })),
)

user.data.value?.name // User

Optional Vue Query adapter

Your contract, now query-ready.

After installing @tanstack/vue-query, generated option factories plug named endpoints directly into Vue Query. There is no second request model to maintain: Vue Query owns server-state behavior while Nuxt Endpoints keeps every request and response aligned with the server contract.

  • GET and HEAD become typed query and infinite-query options; mutations keep typed variables
  • Ordinary Vue Query options keep invalidation, prefetching, optimistic updates, and Devtools standard
  • Keep app-owned setup by default, or opt into a request-scoped Nuxt SSR QueryClient and hydration
Use Vue Query
GET /_endpoints/schema
{
  "openapi": "3.1.0",
  "paths": {
    "/api/users/{id}": {
      "get": {
        "operationId": "getUser",
        "parameters": [
          { "name": "id", "in": "path", "required": true }
        ],
        "responses": {
          "200": { "description": "OK" },
          "404": { "description": "Not found" }
        }
      }
    }
  }
}

OpenAPI that can't go stale.

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.

  • Served at /_endpoints/schema — always matching the code
  • document / extend hatches add auth schemes and extra detail
  • Plain REST: callable from curl, mobile apps, and any other service
Explore the OpenAPI output

Explore by topic.

Getting Started

Install the module, define your first endpoint, and call it with types.

Open guide

Define Endpoints

Write request and response schemas next to the handler.

Open guide

Generated Client

Call server routes by typed path, method, or operation name.

Open guide

Responses

Choose between success bodies, status-aware typed results, and raw Web Responses.

Open guide

Vue Query

Generate typed query, mutation, and infinite-query options for Vue Query from named endpoints, with optional Nuxt SSR setup.

Open guide

OpenAPI

Serve OpenAPI 3.1 from endpoint definitions.

Open guide

Schema Libraries

Zod, Valibot, and Effect Schema are supported without locking the API layer to one vendor.

Open guide

Idempotency

Optional Idempotency-Key replay protection for unsafe endpoints, with an application-owned durable storage contract.

Open guide

Low-level HTTP

Handle files, streams, redirects, proxies, raw Responses, and 204 routes.

Open guide

Incremental Adoption

Convert one route at a time. Every other route keeps working unchanged.

Open guide

Mental Model

One route definition powers the server, client, and documentation.

Open guide

Why Nuxt Endpoints?

The drift problem in plain Nuxt apps, and the single-contract idea behind the module.

Open guide

Comparison

How Nuxt Endpoints relates to Nuxt typed fetch, tRPC, and OpenAPI tooling.

Open guide

Limits

Early alpha, with the important constraints documented.

Open guide