Skip to content

Request Validation

Validate incoming requests with a declarative schema — no external dependencies.

Quick Start

typescript
import bunway, { validate, json } from "bunway";

const app = bunway();
app.use(json());

app.post("/users",
  validate({
    body: {
      name: { required: true, type: "string", min: 2, max: 50 },
      email: { required: true, type: "email" },
      age: { type: "number", min: 18 },
    },
  }),
  (req, res) => {
    res.status(201).json({ created: true });
  }
);

Validation Sources

SourceValidatesExample
bodyreq.bodyPOST/PUT JSON or form data
queryreq.queryURL query parameters
paramsreq.paramsRoute parameters (:id)

Field Rules

RuleTypeDescription
requiredbooleanField must be present and non-empty
typestringType check: "string", "number", "integer", "boolean", "email", "url", "uuid"
minnumberMin length (strings) or min value (numbers)
maxnumberMax length (strings) or max value (numbers)
patternRegExpRegex the value must match
enumunknown[]Allowed values list
custom(value, req) => boolean | string | Promise<...>Custom validator
messagestringCustom error message
trimbooleanTrim whitespace before validation
toLowerCasebooleanConvert to lowercase before validation
toNumberbooleanConvert string to number before validation

Examples

Route parameter validation

typescript
app.get("/users/:id",
  validate({
    params: { id: { required: true, pattern: /^\d+$/ } },
  }),
  (req, res) => res.json({ id: req.params.id })
);

Query validation

typescript
app.get("/search",
  validate({
    query: {
      q: { required: true, min: 1 },
      page: { toNumber: true, type: "number", min: 1 },
      limit: { toNumber: true, type: "number", min: 1, max: 100 },
    },
  }),
  (req, res) => res.json({ results: [] })
);

Async custom validators

typescript
app.post("/register",
  validate({
    body: {
      username: {
        required: true,
        custom: async (value) => {
          const exists = await db.users.findByUsername(value);
          return exists ? "Username is already taken" : true;
        },
      },
    },
  }),
  (req, res) => res.status(201).json({ ok: true })
);

Custom error format

typescript
app.post("/api",
  validate(schema, {
    statusCode: 400,
    errorFormatter: (errors) => ({
      success: false,
      issues: errors.map(e => ({
        path: `${e.source}.${e.field}`,
        message: e.message,
      })),
    }),
  }),
  handler
);

Custom error handler

typescript
app.post("/api",
  validate(schema, {
    onError: (errors, req, res, next) => {
      // Log or pass to error middleware
      next(errors);
    },
  }),
  handler
);

Using with Zod / Yup / Joi

For complex schemas, use the custom validator or write your own middleware:

typescript
import { z } from "zod";

const UserSchema = z.object({
  name: z.string().min(2),
  email: z.string().email(),
});

app.post("/users",
  validate({
    body: {
      name: { custom: (val) => typeof val === "string" && val.length >= 2 },
      email: { custom: (val) => typeof val === "string" && val.includes("@") },
    },
  }),
  handler
);

// Or use Zod directly in a custom middleware:
app.post("/users", (req, res, next) => {
  const result = UserSchema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({ errors: result.error.issues });
  }
  next();
}, handler);

Options

OptionTypeDefaultDescription
abortEarlybooleanfalseStop at first error
statusCodenumber422HTTP status for validation errors
errorFormatter(errors) => unknown{ errors: [...] }Custom error response shape
onError(errors, req, res, next) => voidCustom error handler