Defining Routes
Learn how to define type-safe route endpoints using Taser.js's fluent route builder. Chain query, params, body schemas, middlewares, and response contracts.
Every route file in src/routes/ exports a Route constant created by invoking the router instance builder (t).
Route Builders
The t router instance provides fluent builder methods corresponding to all standard HTTP methods:
t.get(path);
t.post(path);
t.put(path);
t.patch(path);
t.delete(path);
t.options(path);
t.head(path);
t.query(path);
t.any(path, methods);
t.all(path);Filesystem Path vs. Builder Path String
The filesystem location (src/routes/...) is the sole runtime source of truth for routing,
HTTP dispatch, and cascading middleware pipelines. The path string passed to builder methods (e.g.
t.get("/articles"), t.layout("/admin/*")) is used exclusively for TypeScript compile-time
type inference (typing ctx.params) and type checking against ambient project routes
(RoutePath).
Path String vs. File Location Matrix
| Dimension | Filesystem Location (src/routes/...) | Builder Path String (t.get(...), t.layout(...)) |
|---|---|---|
| Runtime Routing | Source of truth: determines URL matching and routing | Ignored at runtime (does not affect dispatch) |
| HTTP Dispatch | Extracted from filename suffix (.get.ts) | Must match factory method (t.get) during AST scan |
| Type Inference | Emits ambient RoutePath union | Infers ctx.params shape via PathParams<Path> |
| Validation Layer | Build-time duplicate route checks | TypeScript compiler (tsc) via ambient RoutePath |
Compile-Time vs. Build-Time Validation
- Build-Time AST Scanning: The router generator verifies that the factory method matches the file verb (e.g.
users.get.tsmust callt.get(...)). It does not reject mismatches between the path argument and the file's derived URL. - Compile-Time Type Checking: Ambient types emitted by
@taserjs/router-clior Vite augmentRouterRegister.RoutePath. Passing an unknown or misspelled route path causes TypeScript (tsc) to report a compile error:
Type '"/artikles"' is not assignable to type 'RoutePath'.- Mismatched Path String Drift: If you provide a path string that exists elsewhere in your project (e.g. calling
t.get("/users/:id")insidesrc/routes/articles.get.ts), runtime routing still dispatches strictly to/articles, butctx.paramswill drift to reflect{ id: string }instead of{}.
Before / After: Aligning Route Path Strings
import { json } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
// ❌ TypeScript error: Type '"/artikles"' is not assignable to type 'RoutePath'.
// ⚠️ Even if ignored, runtime endpoint remains /articles, while type inference breaks.
const GET = t.get("/artikles");
export default GET.handler((ctx) => {
return json({ ok: true });
});import { json } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
// ✅ Correct: Path string matches the filesystem-derived endpoint /articles
const GET = t.get("/articles");
export default GET.handler((ctx) => {
return json({ ok: true });
});Fluent Chaining Methods
Each builder method returns a RouteBuilder that supports fluent method chaining:
| Method | Description |
|---|---|
.query(schema) | Validates query string parameters |
.params(schema) | Validates and coerces path parameters |
.body(schema) | Validates request payloads (JSON by default) |
.body(mode, schema) | Validates payloads with explicit mode (json, form, text, raw) |
.returns({ [status]: schema }) | Enforces compile-time and runtime response contracts |
.use(middleware) | Attaches route-level middleware |
.handler(fn) | Terminal method executing the route logic |
Defining a GET Route
GET, DELETE, OPTIONS, and HEAD routes accept validation schemas for query and params:
import { json } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
const GET = t
.get("/articles")
.query(
z.object({
category: z.string().optional(),
page: z.coerce.number().int().min(1).default(1),
limit: z.coerce.number().int().min(1).max(50).default(20),
}),
)
.returns({
200: z.object({
articles: z.array(
z.object({
id: z.string(),
title: z.string(),
category: z.string(),
}),
),
page: z.number(),
total: z.number(),
}),
});
export type RouteContext = typeof GET.$Infer.Context;
export default GET.handler(async (ctx) => {
const articles = await ctx.db.getArticles(ctx.query);
return json({
articles,
page: ctx.query.page,
total: 100,
});
});Path Parameters & Type Precedence
Path parameters in dynamic routes (such as /tasks/:id or /orgs/:orgId/users/:id) are automatically inferred as string on ctx.params.
When you supply a .params() schema, the validated schema types take precedence and override the default string types with full type coercion:
import { json } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
export default t
.get("/tasks/:id")
.params(
z.object({
id: z.coerce.number(), // ctx.params.id is coerced to number
}),
)
.handler(async (ctx) => {
const task = await ctx.db.getTaskById(ctx.params.id); // ctx.params.id is number
return json(task);
});Any path parameter not explicitly mentioned in the .params() schema retains its inferred string type (for example, /orgs/:orgId/tasks/:id preserves ctx.params.orgId as string).
Defining a POST Route
POST, PUT, PATCH, and QUERY routes accept .body(), .query(), and .params() schemas:
import { created } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
const POST = t
.post("/articles")
.body(
z.object({
title: z.string().min(3).max(120),
content: z.string().min(10),
tags: z.array(z.string()).default([]),
}),
)
.returns({
201: z.object({
id: z.string(),
title: z.string(),
createdAt: z.string(),
}),
400: z.object({ message: z.string() }),
});
export type RouteContext = typeof POST.$Infer.Context;
export default POST.handler(async (ctx) => {
const article = await ctx.db.createArticle(ctx.body);
return created(article);
});JSON & Multipart / File Uploads
The .body() method validates application/json by default and also supports form, text, and
raw modes. See the Standard Schema Validation
Guide for examples
on validating File uploads, sizes, and MIME types.
Route-Level Middlewares
In addition to directory layout middlewares, you can attach route-specific middleware using .use():
import { noContent } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
import { verifySuperAdmin } from "../../middleware/super-admin";
import { rateLimit } from "../../middleware/rate-limit";
export default t
.delete("/admin/purge")
.use(rateLimit({ max: 5, windowMs: 60000 }))
.use(verifySuperAdmin())
.handler(async (ctx) => {
await ctx.db.purgeDeletedRecords();
return noContent();
});Multi-Method Handlers (t.any and t.all)
When an endpoint needs to handle multiple HTTP methods with shared logic, use t.any() or t.all():
import { json, ok } from "@taserjs/router/reply";
import { t } from "@taserjs/router";
// Accepts GET and POST requests
export default t.any("/webhooks/stripe", ["GET", "POST"]).handler(async (ctx) => {
if (ctx.method === "GET") {
return json({ status: "Stripe webhook endpoint active" });
}
const payload = ctx.body;
await handleStripeEvent(payload);
return ok();
});Inferred Route Types ($Infer)
Route builders provide an $Infer namespace for extracting the exact compile-time types of your route context, input arguments, and returns:
import { json, notFound } from "@taserjs/router/reply";
import { z } from "zod";
import { t } from "@taserjs/router";
const GET = t
.get("/users/:id")
.params(z.object({ id: z.string().uuid() }))
.query(z.object({ includeProfile: z.coerce.boolean().default(false) }))
.returns({
200: z.object({ id: z.string(), name: z.string() }),
404: z.object({ message: z.string() }),
});
// Extract context shape (query, params, body, state, singletons):
export type RouteContext = typeof GET.$Infer.Context;
// Extract input arguments shape:
export type RouteInput = typeof GET.$Infer.Input;
// Extract returns shape:
export type RouteOutput = typeof GET.$Infer.Output;
export default GET.handler(async (ctx) => {
const user = await ctx.db.findUser(ctx.params.id);
if (!user) {
return notFound({ message: "User not found" });
}
return json(user);
});Related Guides
File Conventions
Master TanStack Router-style file conventions for REST APIs. Learn flat routes, nested folders, path params ($id), catch-alls ($), and layouts.
Layouts and Middleware
Compose shared logic, authentication guards, and cascading typed state with layout files. Understand middleware execution ordering and state propagation.