Validation

Handlers and middleware are written using the verb helpers defined on the global Run namespace. Beyond declaring which HTTP method a handler responds to, these helpers accept validation options that check, transform, and type a route's path parameters, query string, and request body.

Verb Helpers

Each verb helper (Run.GET, Run.HEAD, Run.POST, Run.PUT, Run.DELETE, Run.PATCH, Run.OPTIONS, and Run.ALL) accepts a handler function or an array of handler functions, optionally preceded by validation options:

Run.POST(handler); // a handler function
Run.POST([auth, handler]); // an array of handlers, composed in order
Run.POST(options, handler); // validation options plus a handler
Run.POST(options, [auth, handler]); // validation options plus an array of handlers
Run.POST(options); // options only, validates and passes through

In a +handler file, export the result under the matching verb name. In a +middleware file, use the default export. Handlers created for a specific verb only run for requests with that method. Run.ALL, available in middleware, runs for every method.

Note

The Run namespace is a global, so no imports are needed in route files. See TypeScript for the types it provides.

Request Bodies

POST, PUT, and PATCH handlers declare how request bodies are read with the json and form options. When either is configured, ctx.body is a promise for the parsed and validated body. Otherwise ctx.body is undefined.

import * as v from "valibot";

export const POST = Run.POST(
  {
    form: v.object({
      name: v.string(),
      age: v.pipe(v.string(), v.toNumber()),
    }),
  },
  async (ctx) => {
    const [data, issues] = await ctx.body;
    if (issues) {
      return Response.json({ issues }, { status: 400 });
    }
    return Response.json(data);
  },
);

Like params and search, ctx.body is lazy. The body is only read, parsed, and validated when the promise is consumed, meaning it is awaited or its .then method is called. A handler that rejects a request early does so without reading the body at all.

Function validators resolve ctx.body to their return value. Standard Schema validators resolve it to a [value, issues] tuple. With no validator, it resolves to the raw parsed body. Requests exceeding the configured size limits below are rejected.

Note

ctx.body is a promise, not a function. Read the parsed body with await ctx.body, there is nothing to call.

json

Handles requests with an application/json content type. The option can be a validator, or an options object:

OptionDefaultDescription
validatorFunction or Standard Schema for the parsed JSON
maxBytes1048576 (1MiB)Maximum request body size in bytes

form

Handles requests with an application/x-www-form-urlencoded or multipart/form-data content type. Repeated field names become arrays. The option can be a validator, or an options object:

OptionDefaultDescription
validatorFunction or Standard Schema for the parsed form
maxBytesmaxFiles * maxFileBytesMaximum request body size in bytes
maxParts1000Maximum number of parts in a multipart request
maxFiles20Maximum number of files in a multipart request
maxFileBytes1048576 (1MiB)Maximum size of each uploaded file in bytes
onFile(ctx, file) => any called for each uploaded file

Standard Schema

When a validator is a Standard Schema, the validated result is a [value, issues] tuple. On success, issues is undefined and value holds the parsed output. On failure, value holds the raw input and issues describes each problem.

TypeScript treats the tuple as a discriminated union: checking issues is what narrows value to the parsed type. Narrowing applies to one read of the tuple at a time, so every place that uses the result needs its own check.

export const POST = Run.POST({ form: signupSchema }, async (ctx, next) => {
  const [signup, issues] = await ctx.body;
  if (issues) {
    return next({ issues });
  }
  return next({ signup }); // downstream sees the narrowed type
});
Tip

Check issues once in the handler and pass the narrowed data to the page with next. The template then receives already-validated values instead of repeating the check.

Merging Options

All options declared for a route, across its middleware and handler, are merged into a single set. When the same option is declared more than once, the last validator wins and object forms are merged shallowly. A maxBytes limit declared in one place combines with a validator declared in another.

This makes middleware a natural home for shared validation. An options-only Run.ALL({ search: ... }) in a +middleware file applies to every route below it.

Next Steps


Contributors

Helpful? You can thank these awesome people! You can also edit this doc if you see any issues or want to improve it.