defineAction()
Create a type-safe server action with Standard Schema validation, middleware, and structured error handling. Returns an H3 event handler that can be used as a Nuxt API route.
Type Signature
function defineAction<
TInputSchema extends StandardSchemaV1,
TOutput,
TCtx = Record<string, unknown>,
TOutputSchema extends StandardSchemaV1 | undefined = undefined,
>(options: DefineActionOptions<TInputSchema, TOutput, TCtx, TOutputSchema>): EventHandlerType Parameters
| Parameter | Constraint | Default | Description |
|---|---|---|---|
TInputSchema | extends StandardSchemaV1 | -- | The Standard Schema type for input validation. Inferred from options.input. |
TOutput | -- | -- | The return type of the handler function. |
TCtx | -- | Record<string, unknown> | The accumulated context type produced by middleware. |
TOutputSchema | extends StandardSchemaV1 | undefined | undefined | The Standard Schema type for output validation. Inferred from options.outputSchema. |
Return Type
ReturnType<typeof defineEventHandler> // H3 EventHandlerThe returned event handler wraps the action lifecycle (parse, validate, middleware, execute, output-validate) and always responds with ActionResult<TOutput>.
Options
interface DefineActionOptions<TInputSchema, TOutput, TCtx, TOutputSchema> {
input?: TInputSchema
outputSchema?: TOutputSchema
middleware?: ActionMiddleware[]
metadata?: ActionMetadata
handleServerError?: (error: Error) => { code: string, message: string, statusCode?: number }
handler: ActionHandler<InferOutput<TInputSchema>, TOutput, TCtx>
}input
- Type:
TInputSchema extends StandardSchemaV1 - Required: No
- Description: A Standard Schema compliant object used to validate the request input. Compatible with Zod (>=3.24), Valibot (>=1.0), ArkType (>=2.1), and any library implementing the Standard Schema specification.
When provided, the raw input is validated before the handler executes. If validation fails, the action returns an ActionResult with code: 'VALIDATION_ERROR', statusCode: 422, and per-field error details in fieldErrors.
If the schema object does not implement the ~standard.validate interface, a TypeError is thrown at runtime.
import { z } from 'zod'
export default defineAction({
input: z.object({
title: z.string().min(1, 'Title is required'),
priority: z.enum(['low', 'medium', 'high']).default('medium'),
}),
handler: async ({ input }) => {
// input is typed as { title: string; priority: 'low' | 'medium' | 'high' }
return { id: Date.now(), ...input }
},
})outputSchema
- Type:
TOutputSchema extends StandardSchemaV1 | undefined - Required: No
- Default:
undefined - Description: A Standard Schema compliant object used to validate the handler return value. If the output does not match, the action returns
code: 'OUTPUT_VALIDATION_ERROR'withstatusCode: 500.
import { z } from 'zod'
export default defineAction({
input: z.object({ id: z.string() }),
outputSchema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
}),
handler: async ({ input }) => {
return await db.user.findUnique({ where: { id: input.id } })
},
})middleware
- Type:
ActionMiddleware[] - Required: No
- Description: An array of middleware functions executed sequentially before the handler. Each middleware can extend the handler context (
ctx) or throw errors to halt execution.
export default defineAction({
middleware: [authMiddleware, rateLimitMiddleware],
input: z.object({ title: z.string() }),
handler: async ({ input, ctx }) => {
// ctx contains values from both middleware
return await db.todo.create({
data: { title: input.title, userId: ctx.user.id },
})
},
})metadata
- Type:
ActionMetadata(alias forRecord<string, unknown>) - Required: No
- Description: Arbitrary key-value data attached to the action for logging, analytics, or authorization. Metadata is not used internally by the runtime but can be read by middleware or external tooling.
export default defineAction({
metadata: { action: 'create-todo', requiredRole: 'editor' },
input: z.object({ title: z.string() }),
handler: async ({ input }) => {
return { id: Date.now(), title: input.title }
},
})handleServerError
- Type:
(error: Error) => { code: string, message: string, statusCode?: number } - Required: No
- Description: Custom error handler invoked when the handler or middleware throws an
Errorinstance that is not anActionError(created viacreateActionError). Allows mapping application-specific exceptions to structured error responses. IfstatusCodeis omitted, defaults to500.
This handler is not called for:
ActionErrorinstances (thrown viacreateActionError) — these are preserved as-is- H3 errors (thrown via
createError) — these are handled separately - Non-Error values (e.g.,
throw "string") — these result inINTERNAL_ERROR
export default defineAction({
handleServerError: (error) => {
if (error.message.includes('UNIQUE constraint')) {
return { code: 'DUPLICATE', message: 'Record already exists', statusCode: 409 }
}
// Never leak internal error messages to clients
return { code: 'SERVER_ERROR', message: 'Something went wrong', statusCode: 500 }
},
input: z.object({ email: z.string().email() }),
handler: async ({ input }) => {
return await db.user.create({ data: { email: input.email } })
},
})handler
- Type:
ActionHandler<InferOutput<TInputSchema>, TOutput, TCtx> - Required: Yes
- Description: The core function that processes the request. Receives a single object parameter with three properties.
type ActionHandler<TInput, TOutput, TCtx> = (params: {
input: TInput
event: H3Event
ctx: TCtx
}) => TOutput | Promise<TOutput>Handler Parameters
input
- Type:
InferOutput<TInputSchema>(orunknownwhen noinputschema is provided) - Description: The validated and typed input data. For
GET/HEADrequests, parsed from query parameters. For all other methods, parsed from the request body.
event
- Type:
H3Event(from theh3package) - Description: The raw H3 event object. Provides access to headers, cookies, request metadata, and all H3 utilities (
getHeader,setCookie,getRequestIP, etc.).
ctx
- Type:
TCtx(defaults toRecord<string, unknown>) - Description: The accumulated context object built by the middleware chain. If no middleware is provided,
ctxis an empty object.
Execution Lifecycle
- Parse input -- For
GET/HEAD, reads query parameters viagetQuery(event). For other methods, reads the JSON body viareadBody(event). Malformed JSON on acontent-type: application/jsonrequest throwsPARSE_ERROR(statusCode: 400). - Validate input -- If
inputschema is provided, validates the parsed input against the Standard Schema~standard.validate()method. Failures returnVALIDATION_ERROR(statusCode: 422) withfieldErrors. - Run middleware -- Executes each middleware in array order. Each middleware should call
next()exactly once. Context passed vianext({ ctx: { ... } })is deep-merged into the existing context (nested objects are recursively merged, arrays are replaced). If a middleware does not callnext(), a warning is logged in development to help detect accidental omissions. - Execute handler -- Calls the handler with
{ input, event, ctx }. - Validate output -- If
outputSchemais provided, validates the handler return value. Failures returnOUTPUT_VALIDATION_ERROR(statusCode: 500). - Return result -- On success, returns
{ success: true, data: TOutput }.
Response Format
Every action returns an ActionResult<TOutput> regardless of outcome.
Success
{
"success": true,
"data": { "id": 1, "title": "Buy milk" }
}Input Validation Error
{
"success": false,
"error": {
"code": "VALIDATION_ERROR",
"message": "Input validation failed",
"statusCode": 422,
"fieldErrors": {
"title": ["Title is required"],
"email": ["Invalid email address"]
}
}
}Output Validation Error
{
"success": false,
"error": {
"code": "OUTPUT_VALIDATION_ERROR",
"message": "Output validation failed",
"statusCode": 500,
"fieldErrors": {
"email": ["Invalid email"]
}
}
}Parse Error
{
"success": false,
"error": {
"code": "PARSE_ERROR",
"message": "Invalid JSON in request body",
"statusCode": 400
}
}Handler / Middleware Error (via createActionError)
{
"success": false,
"error": {
"code": "NOT_FOUND",
"message": "Todo not found",
"statusCode": 404
}
}H3 Error (via createError)
{
"success": false,
"error": {
"code": "SERVER_ERROR",
"message": "Forbidden",
"statusCode": 403
}
}Unhandled Error
Internal details are never leaked to the client. In development mode, the error is logged to the server console.
{
"success": false,
"error": {
"code": "INTERNAL_ERROR",
"message": "An unexpected error occurred",
"statusCode": 500
}
}Examples
Basic Action with Zod
// server/api/todos.post.ts
import { z } from 'zod'
export default defineAction({
input: z.object({
title: z.string().min(1, 'Title is required'),
}),
handler: async ({ input }) => {
const todo = await db.todo.create({ data: input })
return todo
},
})Basic Action with Valibot
// server/api/todos.post.ts
import * as v from 'valibot'
export default defineAction({
input: v.object({
title: v.pipe(v.string(), v.minLength(1, 'Title is required')),
}),
handler: async ({ input }) => {
return { id: Date.now(), title: input.title }
},
})Basic Action with ArkType
// server/api/todos.post.ts
import { type } from 'arktype'
export default defineAction({
input: type({ title: 'string > 0' }),
handler: async ({ input }) => {
return { id: Date.now(), title: input.title }
},
})With Middleware and Output Validation
// server/api/users/[id].get.ts
import { z } from 'zod'
export default defineAction({
input: z.object({ id: z.string().uuid() }),
outputSchema: z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
}),
middleware: [authMiddleware],
metadata: { action: 'get-user' },
handler: async ({ input, ctx }) => {
const user = await db.user.findUnique({ where: { id: input.id } })
if (!user) {
throw createActionError({
code: 'NOT_FOUND',
message: 'User not found',
statusCode: 404,
})
}
return user
},
})Without Input Schema
// server/api/health.get.ts
export default defineAction({
handler: async ({ event }) => {
return {
status: 'ok',
timestamp: Date.now(),
ip: getRequestIP(event),
}
},
})Accessing the H3 Event
// server/api/profile.get.ts
import { z } from 'zod'
export default defineAction({
handler: async ({ event }) => {
const cookie = getCookie(event, 'session_id')
const userAgent = getHeader(event, 'user-agent')
return { cookie, userAgent }
},
})Auto-Import
defineAction is auto-imported in all server routes (server/) when the nuxt-actions module is installed. No manual import is needed.
See Also
- createActionClient -- Builder pattern for shared middleware and schemas
- defineMiddleware -- Create reusable middleware
- createActionError -- Throw structured errors
- Types Reference -- Full type definitions