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.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 req.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 req.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 into
src/.taserjs/routes.gen.ts(by@taserjs/clior the Vite/Next plugin) 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, butreq.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(({ req, 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(({ req, 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, and OPTIONS 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),
}),
);
export default GET.handler(async ({ ctx, req }) => {
const articles = await ctx.db.getArticles(req.query);
return json({
articles,
page: req.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 req.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(), // req.params.id is coerced to number
}),
)
.handler(async ({ ctx, req }) => {
const task = await ctx.db.getTaskById(req.params.id); // req.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 req.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([]),
}),
);
export default POST.handler(async ({ ctx, req }) => {
const article = await ctx.db.createArticle(req.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, req }) => {
if (req.method === "GET") {
return json({ status: "Stripe webhook endpoint active" });
}
const payload = req.body;
await handleStripeEvent(payload);
return ok();
});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.