Core Concepts

Validation

End-to-end type safety with Zod for requests and responses.

Validation

Bklar has first-class support for Zod. When you provide a schema, the framework automatically:

  1. Validates incoming data (Body, Query, or Params).
  2. Returns 400 Bad Request if validation fails.
  3. Infers Types for your handler, giving you full autocompletion.

Request Validation

Pass a schemas object in the route options:

import { z } from "zod";

const createUserSchema = z.object({
  username: z.string().min(3),
  email: z.string().email(),
  age: z.number().int().optional(),
});

app.post("/users", (ctx) => {
  // ctx.body is fully typed!
  const { email, username } = ctx.body;
  return ctx.json({ message: `Created user ${username}` });
}, {
  schemas: { body: createUserSchema },
});

Validating Query & Params

app.get("/search/:category", (ctx) => {
  const { category } = ctx.params;  // Typed
  const { q, limit } = ctx.query;    // Typed
  return ctx.json({ category, q, limit });
}, {
  schemas: {
    params: z.object({
      category: z.enum(["books", "movies"]),
    }),
    query: z.object({
      q: z.string(),
      limit: z.coerce.number().min(1).max(100).default(10),
    }),
  },
});

Response Validation

Bklar also supports response schema validation by status code. Use the responses option:

const userSchema = z.object({
  id: z.number(),
  name: z.string(),
  email: z.string().email(),
});

app.get("/users/:id", (ctx) => {
  return { id: 1, name: "Alice", email: "alice@example.com" };
}, {
  responses: { 200: userSchema },
});

If the response doesn't match the schema, a warning is logged in development. This catches implementation bugs without breaking the response flow — the client still receives the data.

Response validation is non-blocking

Response validation warns on mismatches but doesn't reject the response. This prevents exposing validation internals to clients while still catching bugs during development.

Note on Coercion

URL parameters and Query strings are strictly strings by default. Use z.coerce.number() or z.coerce.boolean() to automatically convert them to the correct type.

On this page