Middleware Validation
Validate incoming headers, auth tokens, and session context at the layout level before requests reach downstream REST API route handlers.
Validation in Taser.js is not limited to route handlers. Layout middleware can validate incoming request data and declare the exact shape of state it injects into child routes.
State Inference & Injection
Layout middleware can validate incoming requests and inject typed state down the pipeline. Any object passed to next({ ... }) is automatically merged into ctx.state and inferred across all child handlers:
import { badRequest, notFound } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
type Organization = {
id: string;
slug: string;
plan: "free" | "pro" | "enterprise";
};
export default t.layout("/orgs").use(async (ctx, next) => {
const orgSlug = ctx.headers.get("x-org-slug");
if (!orgSlug) {
return badRequest({ message: "Missing x-org-slug header" });
}
const org: Organization | null = await ctx.db.findOrgBySlug(orgSlug);
if (!org) {
return notFound({ message: "Organization not found" });
}
// TypeScript infers { organization: Organization } into ctx.state for all downstream routes:
return next({ organization: org });
});Validating Query, Params, and Body in Middleware
Middleware can declare query, params, or body schemas using Standard Schema and fluent middleware() to reject invalid requests early before hitting child routes:
import { middleware, t } from "@taserjs/router";
import { unauthorized } from "@taserjs/router/reply";
import { z } from "zod";
const apiKeyGuard = middleware("/api_v2")
.query(
z.object({
apiKey: z.string().min(32),
}),
)
.handler(async (ctx, next) => {
// ctx.query.apiKey is validated
const keyRecord = await ctx.db.findApiKey(ctx.query.apiKey);
if (!keyRecord) {
return unauthorized({ message: "Invalid API key" });
}
return next({ tier: keyRecord.tier });
});
export default t.layout("/api_v2").use(apiKeyGuard);Validation-Only Middleware (Optional Handler)
When a middleware is used purely for schema validation, parameter coercion, or request parsing, calling .handler() is completely optional:
import { middleware } from "@taserjs/router";
import { z } from "zod";
// No .handler() required!
export const pagination = middleware().query(
z.object({
page: z.coerce.number().default(1),
limit: z.coerce.number().default(20),
}),
);You can pass pagination directly to any layout or route with .use(pagination). Taser.js automatically validates and coerces the query parameters, and downstream handlers receive strongly-typed ctx.query.page and ctx.query.limit without any boilerplate:
import { json } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
import { pagination } from "../middleware/pagination.js";
export default t
.get("/posts")
.use(pagination)
.handler(async (ctx) => {
// ctx.query.page and ctx.query.limit are automatically typed as numbers
const posts = await ctx.db.getPosts({ page: ctx.query.page, limit: ctx.query.limit });
return json(posts);
});Multi-Step Middleware Pipelines
You can chain multiple .use() calls on a layout middleware. Each step enriches ctx.state and subsequent middleware steps immediately receive previously injected state:
import { unauthorized } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
export default t
.layout("/billing")
// Step 1: Validate session & inject userId
.use(async (ctx, next) => {
const token = ctx.headers.get("authorization")?.replace("Bearer ", "");
if (!token) return unauthorized();
return next({ userId: "user_123" });
})
// Step 2: Fetch subscription using userId from Step 1
.use(async (ctx, next) => {
// ctx.state.userId is 100% typed from Step 1!
const sub = await ctx.db.getSubscription(ctx.state.userId);
return next({
hasActiveSubscription: Boolean(sub?.active),
});
});Preconditions (.requires<{ state?, params?, query?, body? }>())
When writing reusable standalone middlewares that assume certain state, params, query, or body was already produced upstream (e.g. an authenticated user or tenant ID), declare a precondition requirement via .requires<{ ... }>() on middleware():
import { middleware } from "@taserjs/router";
import { forbidden } from "@taserjs/router/reply";
type SubscriptionPrecondition = {
hasActiveSubscription: boolean;
};
export const requireActivePlan = middleware()
.requires<{ state: SubscriptionPrecondition }>()
.handler((ctx, next) => {
// TypeScript ensures ctx.state.hasActiveSubscription exists:
if (!ctx.state.hasActiveSubscription) {
return forbidden({ message: "Active subscription required" });
}
return next();
});If you attach .use(requireActivePlan) to a route or layout that has not initialized hasActiveSubscription upstream, TypeScript flags a compile-time type mismatch. Middlewares can also declare requirements on params: { ... }, query: { ... }, and body: { ... }.
Next Steps
Standard Schema Validation
Validate query parameters, path params, request bodies, and headers using Zod, ArkType, Valibot, or any Standard Schema library.
Handling Validation Errors
Catch and format input validation failures using ValidationError. Return structured JSON error envelopes with issue paths and messages.