# CLI & Type Generation Canonical URL: https://taserjs.dev/docs/cli Description: Generate src/.taserjs/routes.gen.ts, scaffold empty route files, and run standalone typechecks with @taserjs/cli. The `@taserjs/cli` package provides the `taser` binary. Use it to scan routes, scaffold blank files, and write the unified manifest at `src/.taserjs/routes.gen.ts` without running a development server. *** ## Installation [#installation] Install as a development dependency: `bash pnpm add -D @taserjs/cli ` `bash npm install -D @taserjs/cli ` `bash bun add -D @taserjs/cli ` `bash yarn add -D @taserjs/cli ` *** ## Project Config (`taserjs.config.ts`) [#project-config-taserjsconfigts] Most projects define paths once with `defineConfig` from `@taserjs/cli`: ```ts title="taserjs.config.ts" import { defineConfig } from "@taserjs/cli"; export default defineConfig({ serverDir: "src", routesDir: "routes", outputDir: ".taserjs", app: "taser.ts", }); ``` With those defaults, routes live in `src/routes/`, the app definition is `src/taser.ts`, and generation writes `src/.taserjs/routes.gen.ts`. *** ## `taser generate` [#taser-generate] Scans the routes directory, scaffolds blank route files with filesystem-synced path strings, and writes `${serverDir}/${outputDir}/routes.gen.ts` (default `src/.taserjs/routes.gen.ts`): compiled `app`, ambient route types, and the composite `AppManifest` for `@taserjs/client`. ```bash # In package.json scripts (when installed as a dev dependency) taser generate [--config ] [--watch] # On-demand via npx npx @taserjs/cli generate [--config ] [--watch] ``` The npm package is `@taserjs/cli`; the executable is `taser`. ### Scaffolding blank files [#scaffolding-blank-files] Empty route files (for example after `touch src/routes/admin/users.get.ts`) are filled with starter boilerplate and the matching path string. Existing non-empty files are never overwritten. ```ts title="src/routes/admin/users.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; const GET = t.get("/admin/users"); export type RouteContext = Parameters[0]; export default GET.handler(() => { return json({ ok: true }); }); ``` *** ## Build vs. Standalone Typecheck [#build-vs-standalone-typecheck] Bundler plugins (`vite build`, `next build`) generate the manifest during compilation — you do not need a separate CLI step in those pipelines. For standalone `tsc --noEmit` in CI or pre-commit hooks, generate first: ```json title="package.json" { "scripts": { "dev": "vite", "build": "vite build", "typecheck": "taser generate && tsc --noEmit" } } ``` *** ## Next Steps [#next-steps] # Typed Client SDK Canonical URL: https://taserjs.dev/docs/client Description: Connect to Taser.js backends with end-to-end type safety using @taserjs/client. Enjoy autocomplete for routes, query schemas, and return types. The `@taserjs/client` package provides a lightweight, zero-codegen proxy client that infers end-to-end type safety from your generated `AppManifest` in `src/.taserjs/routes.gen.ts`. *** ## 1. Initialize the Client [#1-initialize-the-client] Install the client package in your project: ```bash pnpm add @taserjs/client ``` ```bash npm install @taserjs/client ``` ```bash bun add @taserjs/client ``` ```bash yarn add @taserjs/client ``` Initialize the client with your composite `AppManifest` from `src/.taserjs/routes.gen.ts` (or `${serverDir}/.taserjs/routes.gen.ts`): ```ts title="src/lib/api.ts" import { createClient } from "@taserjs/client"; import type { AppManifest } from "./.taserjs/routes.gen.js"; // Next.js / serverDir layouts: // import type { AppManifest } from "@/server/.taserjs/routes.gen"; export const api = createClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api", headers: async () => { const token = localStorage.getItem("auth_token"); return token ? { Authorization: `Bearer ${token}` } : {}; }, }); ``` Pass `AppManifest` from `routes.gen.ts` as the generic argument to `createClient`. The client infers routes, methods (`$get`, `$post`, `$put`, `$patch`, `$delete`, `$options`, `$query`), input schemas, and return contracts without generating static client source files. *** ## Path Segment Conventions [#path-segment-conventions] The client uses nested proxy property access mirroring your URL paths. Certain URL conventions are translated into valid JavaScript property names: | URL Pattern | Client Property Path | Parameter Passing | | :------------------ | :---------------------------- | :---------------------------------------------------------------- | | `/` | `api.$get()` | `api.$get()` | | `/products` | `api.products.$get()` | `api.products.$get()` | | `/users/:id` | `api.users._id.$get()` | `api.users._id.$get({ param: { id: "123" } })` | | `/files/*` | `api.files._splat.$get()` | `api.files._splat.$get({ param: { _splat: "docs/readme.txt" } })` | | `/.well-known/jwks` | `api.$well_known.jwks.$get()` | `api.$well_known.jwks.$get()` | | `/user-profiles` | `api.user_profiles.$get()` | `api.user_profiles.$get()` | *** ## Calling API Routes [#calling-api-routes] ### GET Request with Query Parameters [#get-request-with-query-parameters] ```ts // Calls GET /api/products?category=electronics&page=1 const response = await api.products.$get({ query: { category: "electronics", page: 1, }, }); if (response.ok) { const data = await response.json(); // Auto-inferred from handler reply helpers or returns[200] schema console.log(data.products); } ``` ### Dynamic Path Parameters (`_param`) [#dynamic-path-parameters-_param] For parameterized routes like `src/routes/users/$id.get.ts`: ```ts // Calls GET /api/users/user_456 const response = await api.users._id.$get({ param: { id: "user_456" }, }); if (response.ok) { const user = await response.json(); console.log(user.name); } ``` ### POST Request with JSON Body [#post-request-with-json-body] For routes like `src/routes/posts.post.ts`: ```ts // Calls POST /api/posts with Content-Type: application/json const response = await api.posts.$post({ body: { title: "Building type-safe APIs with Taser.js", content: "Deterministic file routing with cascading context.", }, }); const createdPost = await response.json(); ``` ### Per-Request Options [#per-request-options] Pass a second argument to any HTTP method to configure request-level options like one-off headers, a custom `fetch` implementation, or standard `RequestInit` settings: ```ts const response = await api.posts.$post( { body: { title: "Draft Post" }, }, { headers: { "X-Idempotency-Key": "req_abc123" }, init: { cache: "no-store" }, }, ); ``` *** ## Response Typing & Handling [#response-typing--handling] Every client method returns a `Promise>`, which wraps the standard Web `Response` object: * `res.status` and `res.ok` reflect runtime HTTP status codes. * `res.json()` returns `Promise` with success payload types inferred automatically — **`.returns()` is optional**. By default, `TJson` is inferred from handler reply helpers (`json()`, `ok()`, `created()`, etc.) as the union of successful `ReplyOf` payload types (`200`–`226`). Add `.returns({ 200: schema })` only when you want server-side contract enforcement, runtime response validation, or to override client inference with an explicit schema type. ### Type Resolution Precedence [#type-resolution-precedence] 1. **Handler inference (default)**: When `.returns()` is omitted, `TJson` unions successful `ReplyOf` types from the route handler. 2. **`returns[200]` override**: When `.returns({ 200: schema })` is defined, `TJson` uses that schema's output type. 3. **Fallback**: `unknown` if neither source is available. ```ts const response = await api.users._id.$get({ param: { id: "user_123" }, }); if (response.ok) { const data = await response.json(); // Typed from handler or returns[200] console.log(data.email); } else { console.error("Request failed with status:", response.status); } ``` *** ## Multipart File Uploads (`formBody`) [#multipart-file-uploads-formbody] Use `formBody()` to upload binary files and structured form fields with `multipart/form-data`: ```ts import { formBody } from "@taserjs/client"; const avatarFile = new File([blob], "avatar.png", { type: "image/png" }); // Calls POST /api/users/user_123/avatar with multipart form payload: const response = await api.users._id.avatar.$post({ param: { id: "user_123" }, body: formBody({ label: "Profile Picture", file: avatarFile, }), }); ``` On the backend, your `body` schema validates incoming `File` objects with native file schemas like zod's `z.file()`, checking max size and allowed MIME types. Learn more in the [Multipart & File Validation Guide](/docs/validation/standard-schema#validating-multipart-form-data--file-uploads). *** ## Inferring Request & Response Types [#inferring-request--response-types] Extract TypeScript types from client endpoint methods using helper generics: ```ts import type { InferRequestType, InferResponseType } from "@taserjs/client"; import type { api } from "@/lib/api"; // Infer input arguments for an endpoint: type UserGetInput = InferRequestType; // { param: { id: string }; query?: ... } // Infer successful JSON payload: type UserData = InferResponseType; ``` *** ## Configuration Reference [#configuration-reference] ### Client Initialization Options (`CreateClientOptions`) [#client-initialization-options-createclientoptions] ### Per-Request Options (`ClientRequestOptions`) [#per-request-options-clientrequestoptions] *** ## Next Steps [#next-steps] # Deployment Presets Canonical URL: https://taserjs.dev/docs/deployments Description: Deploy Taser.js APIs across Cloudflare Workers, Vercel, AWS Lambda, Node.js, Bun, Deno, and Netlify using multi-cloud Nitro server presets. Taser.js leverages Nitro's battle-tested preset system to generate optimized production builds tailored for any cloud platform or runtime with zero code changes. *** ## Configuring Presets [#configuring-presets] You can declare your target preset in `nitro.config.ts`: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "node-server", // Change to your deployment target }); ``` Or override dynamically at build time using the `NITRO_PRESET` environment variable: ```bash NITRO_PRESET=cloudflare-module pnpm build ``` *** ## Supported Presets [#supported-presets] | Preset | Target Platform / Runtime | Output Directory | Command / Start | | :------------------ | :------------------------------------------------------------- | :------------------------------- | :--------------------------------------------- | | `node-server` | Standard Node.js Server (Docker, Railway, Render, Fly.io, VPS) | `.output/server/index.mjs` | `node .output/server/index.mjs` | | `node-cluster` | Multi-core Node.js Cluster with worker process management | `.output/server/index.mjs` | `node .output/server/index.mjs` | | `bun` | Bun high-performance JavaScript runtime | `.output/server/index.mjs` | `bun .output/server/index.mjs` | | `deno-server` | Standalone Deno server | `.output/server/index.mjs` | `deno run -A .output/server/index.mjs` | | `deno-deploy` | Deno Deploy global edge network | `.output/server/index.mjs` | Native Deno Deploy GitHub Action | | `cloudflare-module` | Cloudflare Workers & Pages Functions (V8 isolates) | `.output/server/index.mjs` | `wrangler deploy` | | `vercel` | Vercel Serverless & Edge infrastructure | `.output/` (Build Output API v3) | `vercel deploy` | | `aws-lambda` | AWS Lambda with API Gateway or Lambda Function URLs | `.output/server/index.mjs` | Zip & upload or AWS SAM / Serverless Framework | | `netlify` | Netlify Serverless Functions & Edge | `.output/` | Netlify CLI / GitHub Deploy | *** ## Platform Guides [#platform-guides] ### 1. Node Server (Docker / VPS / PaaS) [#1-node-server-docker--vps--paas] For containerized deployments or long-running Node.js instances: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "node-server", }); ``` Example production `Dockerfile`: ```dockerfile FROM node:20-alpine AS builder WORKDIR /app COPY package.json pnpm-lock.yaml ./ RUN corepack enable && pnpm install --frozen-lockfile COPY . . RUN pnpm build FROM node:20-alpine AS runner WORKDIR /app ENV NODE_ENV=production ENV PORT=3000 COPY --from=builder /app/.output .output EXPOSE 3000 CMD ["node", ".output/server/index.mjs"] ``` *** ### 2. Cloudflare Workers [#2-cloudflare-workers] To deploy to Cloudflare's global edge network: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "cloudflare-module", }); ``` Create `wrangler.jsonc` or `wrangler.toml`: ```json title="wrangler.jsonc" { "name": "my-taser-api", "main": "./.output/server/index.mjs", "compatibility_date": "2026-08-01" } ``` Deploy using Wrangler: ```bash pnpm build npx wrangler deploy ``` *** ### 3. Vercel [#3-vercel] To deploy on Vercel: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "vercel", }); ``` Push your repository to GitHub and import into the Vercel Dashboard. Vercel automatically detects the `.output` build directory. *** ### 4. AWS Lambda [#4-aws-lambda] For AWS Lambda deployment: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "aws-lambda", }); ``` The compiled handler at `.output/server/index.mjs` exposes the standard AWS Lambda handler function `handler(event, context)`. *** ## Next Steps [#next-steps] # Introduction Canonical URL: https://taserjs.dev/docs Description: High-performance, file-based REST API router for TypeScript. Zero runtime drift, cascading middleware context, and automatic client generation. **Taser.js** is a modern, type-safe, file-based router designed for scalable Node.js and edge HTTP services. It bridges the gap between clean filesystem architecture and strict type safety. Traditional Node.js routing forces you to write manual route registries, perform unsafe type assertions on `req.user`, and maintain separate client types that drift over time. Taser.js solves these challenges by treating the filesystem as the source of truth, cascading middleware state through directory layouts, and generating a 100% typed client SDK. **Layout Middleware** ```ts title="src/routes/users.ts" import { t } from "@taserjs/router"; import { unauthorized } from "@taserjs/router/reply"; // Applies to all routes under /users/* export default t.layout("/users/*").use(async ({ req, ctx }, next) => { const token = req.headers.get("authorization")?.replace(/^Bearer\s+/i, ""); const user = token ? await ctx.db.findUserByToken(token) : null; if (!user) { return unauthorized({ message: "Unauthorized" }); } return next({ user }); }); ``` **Route Handler** ```ts title="src/routes/users/$id.get.ts" import { json, notFound, forbidden } 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) })); export default GET.handler(async ({ req, ctx, state }) => { // req.params.id is typed as string (UUID) // req.query.includeProfile is typed as boolean // state contains data injected by layout middlewares const isAdmin = await ctx.db.isUserAdmin(state.user.id); if (!isAdmin) { return forbidden({ message: "Forbidden" }); } const user = await ctx.db.findUserById(req.params.id); if (!user) { return notFound({ message: "User not found" }); } return json(user); }); ``` The path string passed to `t.get()`, `t.post()`, `t.layout()`, etc. is generated automatically by the dev watcher (`vite dev`, `next dev`) or `@taserjs/cli` when route files are first created (e.g. via `touch src/routes/...`). You do not need to author this string by hand. *** ## Why Taser.js? [#why-taserjs] *** ## How Taser.js Works [#how-taserjs-works] Taser.js operates around a deterministic, fully type-safe request lifecycle: Taser.js integrates with Vite via `@taserjs/plugin/vite`. Routes are loaded virtually during development with instant HMR and bundled into optimized production artifacts via Nitro deployment presets. *** ## Scaffold with create-taserjs [#scaffold-with-create-taserjs] Get up and running in seconds using the official scaffolding CLI. Customize your stack across five modular dimensions: * **Host Framework**: Standalone Taser.js (`none`), `hono`, `express`, or `fastify`. * **Deployment Preset**: `node-server`, `node-cluster`, `bun`, `deno-server`, `deno-deploy`, `cloudflare-module`, `vercel`, `aws-lambda`, `netlify`, or standalone Vite (`none`). * **Runtime Override**: Explicit `node` or `bun` execution target for self-hosted presets. * **Database & Driver**: Drizzle, Prisma, or Kysely with SQLite, PostgreSQL, or MySQL drivers (`--db :`). * **Standard Add-ons**: Zod, ArkType, or Valibot for schema validation; Pino or Winston for structured logging. ```bash pnpm create taserjs@latest my-api ``` ```bash pnpm create taserjs@latest my-api \ --framework none \ --preset node-server \ --db drizzle:postgres \ --validator zod \ --logger pino \ -y ``` For full flag definitions and project structure options, see the [Quickstart Guide](/docs/getting-started). *** ## AI Agent Skill [#ai-agent-skill] Equip your AI coding assistant (Cursor, Claude Code, Windsurf, GitHub Copilot, Cline, Codex) with expert knowledge of Taser.js file routing conventions, cascading layouts, Standard Schema validation, and typed client RPC. Install the official skill using the open agent skills CLI: ```bash npx skills add taserjs/taserjs ``` ```bash pnpm dlx skills add taserjs/taserjs ``` ```bash bunx skills add taserjs/taserjs ``` ### What the Skill Teaches Your Assistant [#what-the-skill-teaches-your-assistant] * **Filesystem Conventions**: Eliminates hallucinated routes by strictly adhering to verb suffixes (`.get.ts`, `.post.ts`), dynamic parameters (`$id`), splats (`$`), and layout middleware. * **Layout and Middleware Inheritance**: Guides assistants to declare cascading layouts and middleware that flow into child routes via `state` without unsafe type assertions. * **Validation & Return Contracts**: Guides assistants to declare schemas with Zod, ArkType, or Valibot while pairing status codes with standalone reply helpers (`json`, `created`, `notFound`). * **Host Migrations & Client RPC**: Guides incremental migrations from Express, Fastify, and Hono using host pass-through adapters and generates typed `@taserjs/client` calls. ### Example Prompts [#example-prompts] Once installed, your AI assistant will reference Taser.js rules automatically. Try prompts like: ```text "Set up Taser.js API routes in my Next.js project with Zod schema validation, an auth layout, and a typed client." ``` ```text "Create a type-safe POST /api/users endpoint in Taser.js with body validation and compile-time return contracts." ``` ```text "Migrate my existing Express routes to Taser.js using host pass-through without breaking current APIs." ``` *** ## Quick Navigation [#quick-navigation] Explore the documentation to start building or migrating your existing server: # @taserjs/cli Canonical URL: https://taserjs.dev/docs/api-reference/cli Description: CLI reference for taser generate, taserjs.config.ts, and src/.taserjs/routes.gen.ts output. The `@taserjs/cli` package ships the `taser` binary for route scanning, scaffolding, and manifest generation. *** ## Commands [#commands] ### `taser generate` [#taser-generate] Writes `${serverDir}/.taserjs/routes.gen.ts` (defaults to `src/.taserjs/routes.gen.ts`). ```bash taser generate [--config ] [--watch] npx @taserjs/cli generate [--config ] [--watch] ``` *** ## Configuration [#configuration] Config resolution order: 1. `--config ` when provided 2. `taserjs.config.ts` / `.js` / `.mjs` / `.cjs` in the working directory 3. Built-in defaults (`serverDir: "src"`, `routesDir: "routes"`, `outputDir: ".taserjs"`, `app: "taser.ts"`) ```ts title="taserjs.config.ts" import { defineConfig } from "@taserjs/cli"; export default defineConfig({ serverDir: "src", routesDir: "routes", outputDir: ".taserjs", app: "taser.ts", }); ``` *** ## CI Typecheck Pattern [#ci-typecheck-pattern] ```json title="package.json" { "scripts": { "typecheck": "taser generate && tsc --noEmit -p tsconfig.json" } } ``` # @taserjs/client Canonical URL: https://taserjs.dev/docs/api-reference/client Description: Complete API reference for @taserjs/client: createClient options, fetch interop, formBody utilities, and end-to-end TypeScript types. The `@taserjs/client` package provides a lightweight, zero-codegen typed proxy client that infers routes, parameter inputs, and response return shapes directly from your backend router definitions. *** ## Core Functions [#core-functions] ### `createClient(options)` [#createclienttappoptions] Creates an auto-completing typed proxy client. Pass `AppManifest` from `src/.taserjs/routes.gen.ts` as `TApp` to infer endpoints, parameters, query schemas, and return contracts: ```ts function createClient(options?: CreateClientOptions): Client; ``` ```ts import { createClient } from "@taserjs/client"; import type { AppManifest } from "./.taserjs/routes.gen.js"; const api = createClient({ baseUrl: "https://api.example.com/api", }); ``` #### Options (`CreateClientOptions`) [#options-createclientoptions] *** ### `formBody(fields, options?)` [#formbodyfields-options] Encodes fields and file attachments into a `FormData` multipart payload tagged for TypeScript body inference: ```ts function formBody>( value: T, options?: FormBodySerializeOptions, ): FormBody; ``` #### Field Types (`FormBodyField`) [#field-types-formbodyfield] *** ## Per-Request Options (`ClientRequestOptions`) [#per-request-options-clientrequestoptions] Every client method takes an optional second argument for request-specific configuration: ```ts export type ClientRequestOptions = { headers?: Record | (() => Record | Promise>); fetch?: typeof fetch; init?: RequestInit; }; ``` *** ## Client Proxy Calling Signatures [#client-proxy-calling-signatures] The client exposes methods named with `$`: *** ## Path Segment Conventions [#path-segment-conventions] URL paths are accessed through property chaining on the client: * **Dynamic Parameters (`:id`)**: Property is prefixed with `_` (e.g. `api.users._id.$get({ param: { id: "123" } })`). * **Wildcards (`*`)**: Property is `_splat` (e.g. `api.assets._splat.$get({ param: { _splat: "image.png" } })`). * **Leading Dots (`.well-known`)**: Property starts with `$` (e.g. `api.$well_known.jwks.$get()`). * **Hyphens (`user-profiles`)**: Replaced with underscores in property names (e.g. `api.user_profiles.$get()`). * **Root Route (`/`)**: Method called directly on client instance (e.g. `api.$get()`). * **Literal Index (`/index`)**: Property `index` (e.g. `api.index.$get()`). *** ## Response & Type Inference [#response--type-inference] Every client endpoint returns a `ClientResponse`, which extends Web `Response` with a typed `json()` method: ```ts export type ClientResponse = Omit & { json(): Promise; }; ``` ### Type Resolution Precedence [#type-resolution-precedence] `TJson` for `await res.json()` is resolved automatically — **`.returns()` is not required**: 1. **Handler inference (default)**: When `.returns()` is omitted, `TJson` is the union of successful `ReplyOf` types (`200`–`226`) from handler reply helpers (`json()`, `ok()`, `created()`, etc.). 2. **`returns[200]` override**: If the route declares `.returns({ 200: schema })`, `TJson` uses that schema's output type instead of handler inference. 3. **Fallback**: `unknown` if neither source is available. *** ## Exported TypeScript Types & Constants [#exported-typescript-types--constants] ```ts import type { Client, ClientArgsFor, ClientMethodFn, ClientMethodReturn, ClientMethods, ClientRequestOptions, ClientResponse, CreateClientOptions, FormBody, FormBodyField, FormBodyInput, InferRequestType, InferResponseType, InferRoutes, InferSchemaInput, InferSchemaOutput, InferredJsonOutput, SuccessStatusCode, } from "@taserjs/client"; import { createClient, formBody } from "@taserjs/client"; ``` # @taserjs/plugin Canonical URL: https://taserjs.dev/docs/api-reference/plugin Description: API reference for @taserjs/plugin across Vite, Next.js, Nitro, and other bundlers. The `@taserjs/plugin` package provides framework adapters and drives generation of `src/.taserjs/routes.gen.ts` during development and builds. Path layout (`serverDir`, `routesDir`, `outputDir`, `app`) is configured in [`taserjs.config.ts`](/docs/cli) via `@taserjs/cli`. Runtime URL prefixes use `defineTaser().basePath(...)` — never plugin options. *** ## Exports Overview [#exports-overview] | Subpath Export | Description | Target Framework | | :------------------------- | :-------------------------------------------- | :--------------------- | | `@taserjs/plugin/vite` | Vite plugin with HMR and optional HTTP server | Vite, TanStack Start | | `@taserjs/plugin/next` | Next.js configuration wrapper | Next.js App Router | | `@taserjs/plugin/nitro` | Nitro server module | Standalone Nitro, Nuxt | | `@taserjs/plugin/webpack` | Webpack plugin | Webpack | | `@taserjs/plugin/rspack` | Rspack plugin | Rspack | | `@taserjs/plugin/rollup` | Rollup plugin | Rollup | | `@taserjs/plugin/rolldown` | Rolldown plugin | Rolldown | | `@taserjs/plugin/esbuild` | Esbuild plugin | Esbuild | The package root (`@taserjs/plugin`) is not part of the public API. Import a framework subpath. *** ## Shared Options (`TaserPluginOptions`) [#shared-options-taserpluginoptions] Used by Vite, Nitro, Webpack, Rspack, Rollup, Rolldown, and Esbuild: ```ts import { taser } from "@taserjs/plugin/vite"; taser({ server: true, serverEntry: "src/server.ts", config: "./taserjs.config.ts", }); ``` *** ## Next.js (`@taserjs/plugin/next`) [#nextjs-taserjspluginnext] ```ts import type { NextConfig } from "next"; import { createTaser } from "@taserjs/plugin/next"; const nextConfig: NextConfig = {}; const withTaser = createTaser({ config: "./taserjs.config.ts", }); export default withTaser(nextConfig); ``` ### `NextTaserOptions` [#nexttaseroptions] Only project resolution knobs — path layout stays in `taserjs.config.ts`: *** ## Nitro (`@taserjs/plugin/nitro`) [#nitro-taserjspluginnitro] ```ts import { taser } from "@taserjs/plugin/nitro"; export default defineConfig({ modules: [taser({ standalone: true })], }); ``` Uses the same `TaserPluginOptions` as Vite. See the [Nitro guide](/docs/plugins/nitro). # @taserjs/router Canonical URL: https://taserjs.dev/docs/api-reference/router Description: Complete API reference for @taserjs/router: createTaserApp, createContext, fluent RouteBuilder chaining, reply helpers, and core types. The `@taserjs/router` package is the core routing and context engine of the Taser.js ecosystem. *** ## Core Functions [#core-functions] ### `createTaserApp(options?)` [#createtaserappoptions] Creates a new `TaserRouter` builder instance. ```ts function createTaserApp(options?: CreateTaserAppOptions): TaserRouter; ``` #### Options (`TaserAppOptions` on `defineTaser`) [#options-taserappoptions-on-definetaser] Mount `cookie({ secret, ... })` from `@taserjs/router/middleware/cookie` on a layout. See [Cookie Management](/docs/responses/cookies). *** ### `createContext(definition)` [#createcontextdefinition] Defines application boot singletons and per-request metadata. ```ts function createContext(definition: { boot?: () => Promise | TBoot; request?: (req: Request) => Promise | TReq; }): ContextDefinition; ``` *** ### `middleware()` [#middleware] Constructs a standalone, reusable middleware unit with support for fluent schema chaining (`query`, `params`, `body`, `returns`), body mode tagging (`"json" | "form" | "urlencoded"`), layout scoping, multi-layout branch unions, and state preconditions: ```ts // 1. Short Function Signature (Unscoped, Inferred State) const log = middleware((_args, next) => { return next({ traceId: "123" }); }); // 2. Short Function Signature (Layout-Scoped) const adminAuth = middleware("admin", (_args, next) => { return next({ role: "admin" }); }); // 3. Fluent Builder (Unscoped, with Tagged Body Mode & Schemas) const uploadMw = middleware() .query(z.object({ tag: z.string() })) .params(z.object({ id: z.coerce.number() })) .body("form", z.object({ file: z.instanceof(File) })) // tagged bodyMode! .returns({ 400: z.object({ error: z.string() }) }) .handler(async ({ req }, next) => { return next({ uploadedFile: req.body.file }); }); // 4. Fluent Builder (Layout-Scoped with Inherited State) const scopedMw = middleware("admin") .query(z.object({ filter: z.string() })) .handler(async (_args, next) => { return next({ permission: "write" }); }); ``` ### `honoMw()` [#honomw] Adapts a Web Standard / Hono-compatible middleware function `(c, next) => ...` into a Taser.js-compatible middleware handler function `({ ctx, req }, next) => Promise`. ```ts import { honoMw, middleware } from "@taserjs/router"; import { cors } from "hono/cors"; // Used directly in route/layout chains t.get("/hello").use(honoMw(cors())); // Or wrapped in middleware const corsMiddleware = middleware(honoMw(cors())); ``` #### Builder Methods & Generics [#builder-methods--generics] * **`.query(schema)`**: Attaches query parameter validation schema. * **`.params(schema)`**: Attaches path parameter validation schema. * **`.body(schema)` / `.body(mode, schema)`**: Attaches JSON or tagged (`"form" | "urlencoded"`) body validation schema. * **`.requires<{ state?, params?, query?, body? }>()`**: Declares required upstream preconditions across request facets (`state`, `params`, `query`, `body`). Checked at compile time by `.use(...)`. * **`.handler(({ ctx, req, state }, next) => ...)`**: Implements middleware logic and returns a strongly-typed `MiddlewareUnit`. For simple unvalidated middlewares, short function signatures `middleware((args, next) => ...)` and layout-scoped `middleware("/admin", (args, next) => ...)` are also supported. *** ## `TaserRouter` Class Methods [#taserrouter-class-methods] *** ## `t` Route Builders (`t.`) [#t-route-builders-tverb] Imported as `import { t } from "@taserjs/router"`. `t` provides builder factory functions for defining file-based route endpoints and layouts. *** ## `RouteBuilder` Fluent Methods [#routebuilder-fluent-methods] *** ## Tree-Shakeable Reply Helpers (`@taserjs/router/reply`) [#tree-shakeable-reply-helpers-taserjsrouterreply] *** ## `stream` Helpers (`@taserjs/router/stream`) [#stream-helpers-taserjsrouterstream] Edge-compatible Web Standard helpers (no filesystem `file()` helper): *** ## `cookie()` Middleware & `TaserCookieJar` [#cookie-middleware--tasercookiejar] Import `cookie` from `@taserjs/router/middleware/cookie` and mount it on a layout (`t.layout("/*").use(cookie({ secret }))`). Handlers under that layout receive `{ cookies: TaserCookieJar }`. *** ## TypeScript Utility Types [#typescript-utility-types] ```ts import type { BodyMode, ContextDefinition, HeaderKey, HttpMethod, LayoutDefinition, MiddlewareArgs, MiddlewareDefinition, MiddlewareHandler, MiddlewareInput, MiddlewarePreconditions, NextFunction, NotFoundHandler, OnErrorHandler, RequestHeader, ResponseOptions, RouteBodySchema, RouteBuilder, RouteDefinition, RouteHandler, RouteHandlerArgs, RouteSchemas, RouterRegister, StandardSchemaV1, StatusCode, TaserAppOptions, TaserDefinition, TaserHeaders, TaserRequest, ValidationFacet, } from "@taserjs/router"; import type { Cookie, CookieJarOptions, CookieOptions, TaserCookieJar, } from "@taserjs/router/middleware"; ``` Extract handler argument types from the builder before `.handler()` (e.g. `Parameters[0]`) when typing extracted helper functions. # Express Integration Canonical URL: https://taserjs.dev/docs/frameworks/express Description: Add Taser.js file-based routing to an Express application. Coexist with legacy controllers and middleware using the host pass-through architecture. Taser.js integrates with Express applications using the **Host Pass-Through Architecture**. You can keep your existing Express routes, controllers, and middleware active while writing all new endpoints with Taser.js's type-safe file conventions. *** ## How It Works [#how-it-works] 1. **Taser.js First**: Taser.js evaluates requests against your `src/routes/` directory. 2. **Express Fall-Through**: If no Taser.js route matches, the request automatically falls through to your Express application. 3. **404 Handling**: If neither Taser.js nor Express handles the request, Taser.js returns a standard 404 response. ``` [Incoming Request] │ ▼ [Taser.js File Routes] ── Match? ──► [Execute Taser.js Handler] │ (No match) ▼ [Express Host App] ─── Match? ──► [Execute Express Route / Middleware] │ (No match) ▼ [404 Not Found] ``` *** ## Quickstart [#quickstart] ### 1. Scaffold with Express Host [#1-scaffold-with-express-host] ```bash pnpm create taserjs@latest my-express-app --framework express -y ``` ### 2. Configure `src/server.node.ts` [#2-configure-srcservernodets] Create `src/server.node.ts` exporting your Express application: ```ts title="src/server.node.ts" import express from "express"; const app = express(); // Standard Express middleware: app.use(express.json()); // Legacy or host Express routes: app.get("/express-legacy", (req, res) => { res.json({ message: "Hello from legacy Express route!" }); }); export default app; ``` ### 3. Import the Compiled Taser.js App (optional host wiring) [#3-import-the-compiled-taserjs-app-optional-host-wiring] When you need the runnable instance (custom adapters, tests), import it from the generated manifest: ```ts import { app } from "./.taserjs/routes.gen.js"; export default app; // Hono instance compiled from defineTaser() + src/routes/ ``` ### 4. Add Taser.js Routes [#4-add-taserjs-routes] Create `src/routes/users.get.ts`: ```ts title="src/routes/users.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; const GET = t.get("/users"); export default GET.handler(() => { return json({ users: [{ id: "1", name: "Alice" }] }); }); ``` ### 5. Start Development [#5-start-development] ```bash pnpm dev ``` * Requests to `/users` run through Taser.js with complete type safety. * Requests to `/express-legacy` execute your Express controller. *** ## Incremental Migration Strategy [#incremental-migration-strategy] When migrating a large Express codebase to Taser.js: 1. Move shared database pools and Redis clients into `src/context.ts`. 2. Move authentication middleware into root layout `src/routes/$.ts`. 3. Incrementally port controllers into verb files inside `src/routes/`. 4. Keep complex legacy routes in `src/server.node.ts` until ready to migrate. For a comprehensive walkthrough of controller conversions, middleware mapping, and known limitations, read the dedicated [Migration Guide](/docs/getting-started/migration). *** ## Next Steps [#next-steps] # Fastify Integration Canonical URL: https://taserjs.dev/docs/frameworks/fastify Description: Pair Taser.js file routing with Fastify. Coexist with Fastify plugins, hooks, and native route handlers using host pass-through architecture. Taser.js integrates with Fastify applications using the **Host Pass-Through Architecture**. You can run Fastify plugins, hooks, and legacy endpoints alongside Taser.js's file-based route tree. *** ## How It Works [#how-it-works] 1. **Taser.js First**: Taser.js evaluates requests against `src/routes/`. 2. **Fastify Fall-Through**: Unmatched requests fall through to Fastify's internal router. 3. **404 Handling**: If neither handles the request, Taser.js returns 404 Not Found. *** ## Quickstart [#quickstart] ### 1. Scaffold with Fastify Host [#1-scaffold-with-fastify-host] ```bash pnpm create taserjs@latest my-fastify-app --framework fastify -y ``` ### 2. Configure `src/server.node.ts` [#2-configure-srcservernodets] Create `src/server.node.ts` exporting Fastify's routing handler: ```ts title="src/server.node.ts" import Fastify from "fastify"; const app = Fastify({ logger: true }); // Existing Fastify endpoints: app.get("/fastify-legacy", async () => { return { framework: "Fastify", status: "online" }; }); // Ensure Fastify plugins and routes are loaded before export: await app.ready(); export default app.routing; ``` ### 3. Import the Compiled Taser.js App (optional host wiring) [#3-import-the-compiled-taserjs-app-optional-host-wiring] ```ts import { app } from "./.taserjs/routes.gen.js"; export default app; // Hono instance compiled from defineTaser() + src/routes/ ``` ### 4. Add Taser.js Routes [#4-add-taserjs-routes] Create `src/routes/health.get.ts`: ```ts title="src/routes/health.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; const GET = t.get("/health"); export default GET.handler(() => { return json({ ok: true, timestamp: Date.now() }); }); ``` ### 5. Start Development [#5-start-development] ```bash pnpm dev ``` For a comprehensive walkthrough of Fastify plugin and route conversion, middleware mapping, and known limitations, check out the dedicated [Migration Guide](/docs/getting-started/migration). *** ## Next Steps [#next-steps] # Native Fetch Frameworks Canonical URL: https://taserjs.dev/docs/frameworks/fetch-native Description: Run Taser.js alongside any Web Standard or Fetch-native host framework (Hono, Elysia, HatTip, or native Web Fetch handlers) using host pass-through dispatching. Taser.js is built entirely on Web Standard `Request` and `Response` interfaces. Using the **Host Pass-Through Architecture**, you can pair Taser.js's file-based routing and cascading type safety with **any Web Standard / Fetch-native framework** or existing fetch handler. *** ## Supported Fetch-Native Hosts [#supported-fetch-native-hosts] Because Taser.js evaluates standard Web `Request` objects, any framework or library exposing a standard `.fetch(request)` interface or `{ fetch: (req: Request) => Response | Promise }` handler is supported out of the box: * **Hono**: Ultra-fast web standard framework for Node, Bun, Deno, and Cloudflare. * **Elysia**: Ergonomic, type-safe web framework for Bun. * **HatTip**: Minimalist set of modular web-standard packages. * **Itty Router**: Tiny router for JavaScript environments. * **Pure Web Standard Fetch**: Any plain function receiving `Request` and returning `Response`. *** ## How Host Pass-Through Works [#how-host-pass-through-works] When an incoming HTTP request reaches your server: 1. **Taser.js Route Check**: If the URL matches a file route in `src/routes/`, Taser.js dispatches the request with complete compile-time type safety. 2. **Host App Pass-Through**: If no Taser.js route matches, the request falls through directly to your exported application in `src/server.ts`. 3. **404 Not Found**: If neither Taser.js nor the host framework handles the request, Taser.js returns a standard 404 response. ``` [Incoming Request] │ ▼ [Taser.js File Routes] ── Match? ──► [Execute Taser.js Route Handler] │ (No match) ▼ [Fetch Host App] ───── Match? ──► [Execute Host App (Hono / Elysia / Web Fetch)] │ (No match) ▼ [404 Not Found] ``` *** ## Quickstart [#quickstart] ### 1. Configure `src/server.ts` [#1-configure-srcserverts] Create `src/server.ts` exporting your preferred Fetch-native application: ```ts title="src/server.ts" import { Hono } from "hono"; const app = new Hono(); // Existing or custom Hono endpoints: app.get("/host-info", (c) => { return c.json({ framework: "Hono", status: "online" }); }); export default app; ``` ```ts title="src/server.ts" import { Elysia } from "elysia"; const app = new Elysia(); // Existing or custom Elysia endpoints: app.get("/host-info", () => ({ framework: "Elysia", status: "online" })); export default app; ``` ```ts title="src/server.ts" export default { async fetch(request: Request): Promise { const url = new URL(request.url); if (url.pathname === "/host-info") { return Response.json({ host: "Web Standard Fetch", status: "online" }); } return new Response("Not Found", { status: 404 }); }, }; ``` ### 2. Add Taser.js Routes [#2-add-taserjs-routes] Create file-based routes inside `src/routes/`: ```ts title="src/routes/users.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; const GET = t.get("/users"); export default GET.handler(() => { return json({ users: ["Alice", "Bob"] }); }); ``` ### 3. Import the Compiled Taser.js App [#3-import-the-compiled-taserjs-app] Host pass-through is assembled into the generated entry. For direct access: ```ts import { app } from "./.taserjs/routes.gen.js"; export default app; ``` ### 4. Start Development Server [#4-start-development-server] ```bash pnpm dev ``` * `GET http://localhost:3000/users` is dispatched through Taser.js file routing. * `GET http://localhost:3000/host-info` is handled by your exported host application. *** ## Reusing Web Standard Middleware in Taser.js [#reusing-web-standard-middleware-in-taserjs] Using the `honoMw()` helper, you can adapt any native Web Standard and Fetch-native middleware function into Taser.js: ```ts title="src/routes/$.ts" import { middleware, honoMw, t } from "@taserjs/router"; import { secureHeaders } from "hono/secure-headers"; // Wrap any Web Standard middleware seamlessly using honoMw() const securityHeadersMiddleware = middleware(honoMw(secureHeaders())); export default t.layout("/*").use(securityHeadersMiddleware); ``` *** ## Next Steps [#next-steps] # Standalone API Canonical URL: https://taserjs.dev/docs/frameworks/standalone Description: Build standalone, zero-host HTTP APIs with pure Taser.js, Vite, and Nitro. Maximum throughput, zero boilerplate, and web-standard Request/Response execution. The **Standalone** architecture is the default and recommended way to build backend services with Taser.js. In this mode, you do not need Express, Fastify, or any other host web framework. Taser.js handles request dispatching, middleware pipelines, input validation, and responses directly using high-performance web standards. *** ## Why Standalone? [#why-standalone] *** ## Quickstart [#quickstart] ### 1. Scaffold a Standalone Project [#1-scaffold-a-standalone-project] ```bash pnpm create taserjs@latest my-api --framework none --preset node-server -y ``` ### 2. Project Files [#2-project-files] A standalone project contains no host server files. Import the compiled app from the generated manifest when you need a programmatic entry: ``` my-api/ ├── vite.config.ts # Plugins: [taser(), nitro()] ├── nitro.config.ts # Deployment preset ├── src/ │ ├── taser.ts # defineTaser() definition (TaserDefinition) │ ├── context.ts # Boot and request context │ ├── routes/ # File-based endpoints │ └── .taserjs/ │ └── routes.gen.ts # export { app } — runnable Hono instance └── package.json ``` ```ts title="Programmatic access" import { app } from "./.taserjs/routes.gen.js"; // app is the compiled Hono instance produced from defineTaser() + your routes export default app; ``` ### 3. Run Development Server [#3-run-development-server] ```bash pnpm dev ``` Taser.js starts on `http://localhost:3000`. ### 4. Build for Production [#4-build-for-production] ```bash pnpm build ``` Depending on your deployment mode, Taser.js packages your output into `.output/` (with Nitro presets) or `dist/` (with standalone Vite). *** ## Related Guides [#related-guides] # Core Concepts Canonical URL: https://taserjs.dev/docs/getting-started/core-concepts Description: Master Taser.js architecture: four core pillars, request lifecycle flow, context injection, cascading middleware, and compiler-enforced return contracts. Taser.js is designed to eliminate the ambiguity and unsafe typecasting common in Node.js backend development. To get the most out of Taser.js, it helps to understand its four architectural pillars. *** ## The Four Pillars [#the-four-pillars] *** ## The Request Lifecycle [#the-request-lifecycle] Every HTTP request handled by Taser.js flows through a deterministic, type-safe pipeline: ### 1. Platform Resolution [#1-platform-resolution] When an incoming request reaches your application (Vite Standalone, Nitro presets, Next.js, or an Express, Fastify, or Fetch-native host), the host dispatches to the generated Hono `app` via `app.fetch()` with a Web Standard `Request` object. ### 2. Context Initialization [#2-context-initialization] Taser.js evaluates the application context configured in `src/context.ts`: * **Boot Context**: Singletons created once at application startup (database connections, Redis clients, external API clients). * **Request Context**: Properties created per request (unique `requestId`, incoming timestamp, user agent). ### 3. Cascading Middleware Execution [#3-cascading-middleware-execution] Taser.js runs layout middleware from outermost to innermost: * Root layout (`src/routes/$.ts`) * Parent directory layouts (for example, `src/routes/admin.ts`) * Pathless layouts (for example, `src/routes/admin/_auth.ts`) Any state returned by middleware (such as `next({ user: currentUser })`) merges cleanly into the **`state`** facet for downstream handlers. ### 4. Input Validation [#4-input-validation] Before the route handler executes, Taser.js validates `req.params`, `req.query`, and `req.body` against their declared schemas (`req.headers` stays a raw Web `Headers` accessor). If validation fails, a `ValidationError` is raised and converted to a built-in **422** response. It does **not** reach `defineTaser().onError()` — customize validation envelopes in middleware `try/catch` instead. ### 5. Handler Execution [#5-handler-execution] The terminal handler receives facet-split arguments: **`req`** (HTTP inputs), **`ctx`** (application context from `createContext()`), **`state`** (cascaded layout data), and any **provided services** (for example `cookies` from `cookie()` middleware). ### 6. Response Contract Verification [#6-response-contract-verification] At compile time, TypeScript checks that the value returned from `json()` satisfies the schema declared in `.returns()`. If runtime response validation is enabled, Taser.js also verifies outgoing payloads in development. Contract failures raise `ResponseValidationError` (built-in **500**) and also bypass `.onError()`. *** ## Application Definition vs Compiled Runtime [#application-definition-vs-compiled-runtime] `src/taser.ts` exports an uninstantiated `TaserDefinition` from `defineTaser()` — configuration only (`context`, `basePath`, `notFound`, `onError`, `response`). It does **not** create the Hono app. ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; import { context } from "./context.js"; export default defineTaser({ response: { validate: true }, }).context(context); ``` The plugin/CLI generates `src/.taserjs/routes.gen.ts`, which imports your definition, compiles routes, and exports: * **`app`** — the runnable Hono instance (also `default`) * **Ambient `RouterRegister` augmentations** — `RoutePath`, layout hierarchy, and `AppContext` * **Manifest types** — including the composite `AppManifest` used by `@taserjs/client` Host adapters and catch-all handlers import `app` from that generated file (for example `import { app } from "./.taserjs/routes.gen.js"`). With a standard `"include": ["src/**/*"]` in `tsconfig.json`, generated types under `src/.taserjs/` are picked up automatically — no separate `.taser` include paths. *** ## Handler facets (`req`, `ctx`, `state`) [#handler-facets-req-ctx-state] Route handlers and middleware use **facet-split** arguments instead of a single flattened context object: ```ts title="src/routes/admin/users/$id.get.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const GET = t .get("/admin/users/:id") .params(z.object({ id: z.string() })) .query(z.object({ detailed: z.coerce.boolean().default(false) })); export default GET.handler(async ({ req, ctx, state }) => { // Application context (boot + request hooks from createContext) const db = ctx.db; const requestId = ctx.requestId; // Cascaded layout middleware state const adminUser = state.currentUser; // Validated HTTP inputs on req const userId = req.params.id; const isDetailed = req.query.detailed; // Raw Web Standard request surface const method = req.method; const authHeader = req.headers.get("authorization"); return json({ userId, adminUser, requestId, isDetailed, method, authHeader }); }); ``` ### Facet reference [#facet-reference] ### Cookie jar (`cookie()` middleware) [#cookie-jar-cookie-middleware] Cookies are not available until you mount `cookie()` on a layout. Handlers under that layout then receive a Cookie Jar Instance as `{ cookies }`: ```ts title="src/routes/$.ts" import { cookie } from "@taserjs/router/middleware/cookie"; import { t } from "@taserjs/router"; export default t.layout("/*").use(cookie({ secret: process.env.COOKIE_SECRET! })); ``` ```ts title="src/routes/me.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/me").handler(({ cookies }) => { return json({ theme: cookies.get("theme") ?? "light" }); }); ``` *** ## Context vs Middleware State [#context-vs-middleware-state] Understanding when to place properties in **Application Context (`createContext`)** versus **middleware `state`** is essential for maintaining clean architecture: | Architectural Dimension | Application Context (`createContext`) | Middleware state (`state`) | | :---------------------- | :------------------------------------------------------------------ | :----------------------------------------------------------------------- | | **Location** | `src/context.ts` (attached in `src/taser.ts`) | Folder layouts (`admin.ts`, `dashboard.ts`, `_auth.ts`, …) | | **Lifecycle** | Boot (singletons) & Per-Request hooks | Per-request during middleware pipeline traversal | | **Scope** | **Global**: Accessible across every route and middleware in the app | **Scoped**: Accessible only to child routes located under that directory | | **Access Syntax** | `ctx.db`, `ctx.logger`, `ctx.requestId` | `state.user`, `state.session`, `state.jwtPayload` in handlers | | **Declaration Type** | `defineTaser().context(context)` | Inferred from `return next({ ... })` in layouts | | **Primary Use Cases** | Database pools, Redis clients, queue producers, correlation IDs | Authenticated users, RBAC permissions, tenant metadata | ### Code Comparison [#code-comparison] #### 1. Global Infrastructure in `src/context.ts` [#1-global-infrastructure-in-srccontextts] Use `createContext` to inject server singletons and universal request properties: ```ts title="src/context.ts" import { createContext } from "@taserjs/router"; import { dbPool } from "./db.js"; export const context = createContext({ // Initialized once at server boot boot: () => ({ db: dbPool, logger: console, }), // Evaluated once per incoming HTTP request request: (req) => ({ requestId: req.headers.get("x-request-id") ?? crypto.randomUUID(), startTime: Date.now(), }), }); ``` #### 2. Conditional Scoped State in `src/routes/admin/$.ts` [#2-conditional-scoped-state-in-srcroutesadmints] Use `next({ ... })` in layout middleware to compute and pass typed state downstream: ```ts title="src/routes/admin/$.ts" import { t } from "@taserjs/router"; // State returned in next() cascades into all routes inside src/routes/admin/ export default t.layout("/admin/*").use(async ({ ctx, req }, next) => { const token = req.headers.get("authorization"); if (!token) { throw new Error("Unauthorized"); } const user = await ctx.db.verifyToken(token); return next({ user, // Typed User object available as state.user downstream role: "admin" as const, }); }); ``` Unlike traditional Express where you must declare global `Express.Request` namespace overrides, Taser.js infers middleware types through lexical scope and folder hierarchies. *** ## Related Guides [#related-guides] # Quickstart Canonical URL: https://taserjs.dev/docs/getting-started Description: Create and run a new Taser.js project in under a minute with create-taserjs. Choose your favorite server adapter, schema validator, ORM, and logger. The quickest way to get started with Taser.js is using the official `create-taserjs` scaffolding tool. It generates a pre-configured, production-ready project with TypeScript, hot-reloading, route watching, and your choice of tools. *** ## Prerequisites [#prerequisites] Before creating a project, make sure your development environment meets these requirements: * **Node.js**: Version `20.19.0` or higher * **Package Manager**: `npm`, `pnpm`, `yarn`, or `bun` * **TypeScript**: Version `5.0` or higher *** ## Create a New Project [#create-a-new-project] ### Run the Scaffolding Command [#run-the-scaffolding-command] Run the interactive initializer in your terminal: ```bash pnpm create taserjs@latest ``` ```bash npm create taserjs@latest ``` ```bash bun create taserjs@latest ``` ```bash yarn create taserjs ``` ### Choose Your Stack [#choose-your-stack] The interactive wizard will guide you through selecting your preferred tools: 1. **Project Name**: Enter a directory name (for example, `my-taser-api`). 2. **Host Framework**: * **None (Standalone)**: Pure Taser.js with high-performance virtual routing and zero host overhead (default). * **Hono**: Lightweight web standard framework for Node, Bun, Deno, and edge runtimes. * **Express**: Battle-tested Node.js web framework with pass-through routing. * **Fastify**: High-throughput Node.js framework with pass-through routing. 3. **Deployment Target / Preset**: * **Node Server** (`node-server`, `node-cluster`): Standard Node.js environments (default: `node-server`). * **Standalone Vite** (`none`): Direct `vite build` with production serve shim without Nitro. * **Serverless & Edge**: `cloudflare-module`, `vercel`, `aws-lambda`, `netlify`. * **Runtimes**: `bun`, `deno-server`, `deno-deploy`. 4. **Runtime Override** (Optional): * Override runtime for self-hosted targets (`node`, `bun`, or preset default). 5. **Database / ORM** (Optional): * **Drizzle**, **Prisma**, **Kysely**, or None (supports `sqlite`, `postgres`, or `mysql` drivers). 6. **Logger** (Optional): * **Pino**, **Winston**, or standard Console. 7. **Schema Validator** (Optional): * **Zod**: TypeScript-first schema declaration and validation. * **ArkType**: TypeScript-native syntax with runtime performance. * **Valibot**: Modular, lightweight, tree-shakeable schema library. You can pass CLI flags directly for automated or CI environments: ```bash pnpm create taserjs@latest my-api \ --framework none \ --preset node-server \ --db drizzle:postgres \ --validator zod \ --logger pino \ -y ``` ### CLI Flags Reference [#cli-flags-reference] | Flag | Description | Values / Syntax | Default | | :---------------------- | :---------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :--------------------------- | | `--framework` | Host server framework | `none`, `hono`, `express`, `fastify` | `none` | | `--preset`, `-p` | Deployment preset (Nitro target or `none`) | `none`, `node-server`, `node-cluster`, `bun`, `deno-server`, `deno-deploy`, `cloudflare-module`, `vercel`, `aws-lambda`, `netlify` | `node-server` | | `--runtime` | Explicit runtime override (for self-hosted presets) | `node`, `bun`, `deno` | Implied by preset | | `--db` | Database ODM and optional driver (`odm:driver`) | `drizzle`, `prisma`, `kysely` (optionally `:postgres`, `:sqlite`, `:mysql`) | None (omitted = no database) | | `--validator` | Schema validation library | `zod`, `arktype`, `valibot` | None | | `--logger` | Structured logger integration | `pino`, `winston` | None | | `-y`, `--yes` | Non-interactive mode using defaults for omitted flags | `boolean` | `false` | | `--noInstall` | Skip automatic package installation | `boolean` | `false` | | `--json` (with name) | Output scaffold result metadata as JSON | `boolean` | `false` | | `--json` (without name) | Output capability catalog as JSON | `boolean` | `false` | #### Database Flag (`--db`) Semantics [#database-flag---db-semantics] The `--db` flag controls database ORM and driver installation. The behavior depends on whether the flag is omitted, passed without a driver suffix, or passed with a full `odm:driver` specification: | Invocation Syntax | ODM Configured | Driver Configured | Scaffolding Behavior | | :---------------------- | :------------- | :----------------------------- | :------------------------------------------------------------------------------- | | **Omitted entirely** | *None* | *None* | No database packages, schemas, or connection clients are installed or generated. | | `--db drizzle` | `drizzle` | `sqlite` *(default)* | Defaults driver to `sqlite`. Generates SQLite schema and client files. | | `--db prisma` | `prisma` | `sqlite` *(default)* | Defaults driver to `sqlite`. Generates SQLite Prisma schema. | | `--db kysely` | `kysely` | `sqlite` *(default)* | Defaults driver to `sqlite`. Generates SQLite Kysely database client. | | `--db drizzle:postgres` | `drizzle` | `postgres` | Installs PostgreSQL driver (`pg`) and generates PostgreSQL Drizzle schema. | | `--db drizzle:mysql` | `drizzle` | `mysql` | Installs MySQL driver (`mysql2`) and generates MySQL Drizzle schema. | When `--db` is omitted, the scaffolded project does not include database dependencies or config files. When a database ODM is provided without a colon and driver suffix (for example, `--db drizzle`), create-taserjs automatically selects `sqlite` as the default driver. #### JSON Flag (`--json`) Output Schemas [#json-flag---json-output-schemas] The `--json` flag operates in two distinct modes depending on whether a project name is supplied: | Mode | Command Example | Output Purpose | Output Destination | | :--------------------- | :-------------------------------------------- | :--------------------------------------------------------- | :----------------- | | **Scaffold Metadata** | `pnpm create taserjs@latest my-api --json -y` | Emits resolved project configuration metadata | `stdout` | | **Capability Catalog** | `pnpm create taserjs@latest --json` | Emits available frameworks, presets, runtimes, and add-ons | `stdout` | ##### 1. Scaffold Result Metadata (`--json` with Project Name) [#1-scaffold-result-metadata---json-with-project-name] When a project name is provided, `--json` suppresses terminal animations and prints the scaffolded project configuration as JSON: ```bash pnpm create taserjs@latest my-api \ --framework none \ --preset node-server \ --db drizzle:postgres \ --validator zod \ --logger pino \ --json -y ``` ```json title="Output (Scaffold Metadata JSON)" { "projectName": "my-api", "targetDir": "/path/to/my-api", "framework": "none", "preset": "node-server", "db": "drizzle", "driver": "postgres", "logger": "pino", "validator": "zod" } ``` ##### 2. Capability Catalog (`--json` without Project Name) [#2-capability-catalog---json-without-project-name] When invoked without a project name, `--json` outputs the machine-readable capabilities catalog describing all supported frameworks, deployment presets, runtime environments, databases, loggers, and validators: ```bash pnpm create taserjs@latest --json ``` ```json title="Output (Capabilities Catalog JSON)" { "frameworks": [ "none", "hono", "express", "fastify" ], "deployTargets": [ "none", "node-server", "node-cluster", "bun", "deno-server", "deno-deploy", "cloudflare-module", "vercel", "aws-lambda", "netlify" ], "runtimes": [ "node", "bun", "deno" ], "db": { "odms": [ "drizzle", "prisma", "kysely" ], "drivers": [ "sqlite", "postgres", "mysql" ], "defaultDriver": "sqlite" }, "loggers": [ "pino", "winston" ], "validators": [ "zod", "arktype", "valibot" ] } ``` ### Start the Development Server [#start-the-development-server] Navigate to your new project directory, install dependencies, and start the development server: ```bash cd my-taser-api pnpm install pnpm dev ``` ```bash cd my-taser-api npm install npm run dev ``` ```bash cd my-taser-api bun install bun dev ``` ```bash cd my-taser-api yarn install yarn dev ``` The development server starts with Vite. Route changes inside `src/routes/` are updated virtually with instant HMR, and the unified manifest is written to `src/.taserjs/routes.gen.ts`. *** ## Project Structure Overview [#project-structure-overview] A freshly scaffolded Taser.js project contains a clean, modern layout: ### Key Files Explained [#key-files-explained] * **`vite.config.ts`**: Connects the `taser()` plugin from `@taserjs/plugin/vite` and `nitro()` for zero-config builds (or pure Vite in standalone `none` preset). * **`nitro.config.ts`** *(conditional)*: Generated when targeting non-default deployment presets (e.g. `cloudflare-module`, `vercel`, `aws-lambda`, `netlify`). Omitted for default `node-server` and standalone `none`. * **`src/server.ts` / `src/server.node.ts`** *(conditional)*: Generated when a host framework is configured (`src/server.ts` for Hono, `src/server.node.ts` for Express or Fastify). * **`src/taser.ts`**: Exports `defineTaser()` configuration (`TaserDefinition`) — context, `.onError()`, `.notFound()`, and response options. Does not instantiate the Hono app. ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; import { context } from "./context.js"; export default defineTaser({ response: { validate: true }, }).context(context); ``` * **`src/context.ts`**: Defines application singletons (database, logger, external services) and request-scoped context. * **`src/routes/`**: Contains your API endpoints (`index.get.ts`, `health.get.ts`) and root layout middleware (`$.ts`). * **`src/.taserjs/routes.gen.ts`**: Generated unified entry exporting the runnable `app`, ambient route types, and `AppManifest` for `@taserjs/client`. *** ## Add Your First Route [#add-your-first-route] To create a new endpoint, add a file in the `src/routes/` directory. Create `src/routes/hello.get.ts`: ```ts title="src/routes/hello.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/hello").handler(() => { return json({ message: "Hello from Taser.js!", timestamp: new Date().toISOString(), }); }); ``` Save the file. Vite hot-reloads the new route immediately without restarting the dev server. Test your new endpoint in another terminal or browser: ```bash curl http://localhost:3000/hello ``` Response: ```json { "message": "Hello from Taser.js!", "timestamp": "2026-08-25T00:00:00.000Z" } ``` The path string passed to `t.get()`, `t.post()`, `t.layout()`, etc. is generated automatically by the dev watcher (`vite dev`, `next dev`) or `@taserjs/cli` when route files are first created (e.g. via `touch src/routes/...`). You do not need to author this string by hand. *** ## Next Steps [#next-steps] Now that your project is running, explore these guides to build out your application: # Manual Installation Canonical URL: https://taserjs.dev/docs/getting-started/manual-installation Description: Step-by-step guide to installing and configuring Taser.js manually with Vite, Nitro deployment presets, Next.js, and host pass-through dispatching. If you have an existing project or prefer configuring dependencies manually without the `create-taserjs` wizard, this guide provides complete step-by-step instructions. Taser.js is **Vite-native** by design. You can run Taser.js as a standalone server, pair it with **Nitro** for multi-cloud deployment presets, or enable **Host Pass-Through** to dispatch alongside existing frameworks. *** ## 1. Core Installation (Vite-Native Backend) [#1-core-installation-vite-native-backend] At its foundation, Taser.js operates as a Vite compiler plugin (`@taserjs/plugin/vite`). It turns your `src/routes/` directory into virtual modules with instant Hot Module Replacement (HMR) and ambient TypeScript generation. ### Install Core Dependencies [#install-core-dependencies] Install `@taserjs/router`, `srvx`, `@taserjs/plugin`, `vite`, and your preferred schema validator (such as `zod`): ```bash pnpm add @taserjs/router @taserjs/runtime srvx zod pnpm add -D @taserjs/plugin vite ``` ```bash npm install @taserjs/router @taserjs/runtime srvx zod npm install -D @taserjs/plugin vite ``` ```bash bun add @taserjs/router @taserjs/runtime srvx zod bun add -d @taserjs/plugin vite ``` ```bash yarn add @taserjs/router @taserjs/runtime srvx zod yarn add -D @taserjs/plugin vite ``` ### Configure `package.json` [#configure-packagejson] Configure your project scripts: ```json title="package.json" { "name": "my-taser-app", "type": "module", "scripts": { "dev": "vite", "build": "vite build", "start": "node dist/serve.mjs", "typecheck": "tsc --noEmit -p tsconfig.json" } } ``` ### Configure TypeScript (`tsconfig.json`) [#configure-typescript-tsconfigjson] Include `src/**/*` so TypeScript picks up both your sources and the generated manifest under `src/.taserjs/`: ```json title="tsconfig.json" { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true, "skipLibCheck": true, "isolatedModules": true, "noEmit": true }, "include": ["src/**/*", "taserjs.config.ts", "vite.config.ts"] } ``` ### Configure `vite.config.ts` [#configure-viteconfigts] Attach the `taser()` plugin to your Vite configuration: ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [taser()], }); ``` ### Create Router Instance & Context [#create-router-instance--context] Create `src/context.ts` for boot singletons and request-scoped state: ```ts title="src/context.ts" import { createContext } from "@taserjs/router"; export const context = createContext({ boot: () => ({ logger: console, }), request: () => ({ requestId: crypto.randomUUID(), }), }); ``` Create `src/taser.ts` with an uninstantiated `defineTaser()` definition. The plugin compiles this into the runnable Hono `app` inside `src/.taserjs/routes.gen.ts`: ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; import { context } from "./context.js"; export default defineTaser({ response: { validate: true }, }).context(context); ``` ### Add Your First Route [#add-your-first-route] Create `src/routes/index.get.ts`: ```ts title="src/routes/index.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/").handler(({ ctx }) => { return json({ message: "Welcome to Taser.js!", requestId: ctx.requestId }); }); ``` Start the Vite development server: ```bash pnpm dev ``` *** ## 2. Adding Nitro for Multi-Cloud Deployment Presets [#2-adding-nitro-for-multi-cloud-deployment-presets] **Nitro** acts as the production deployment platform for Taser.js. By adding `nitro()` to your `vite.config.ts`, you can compile your Vite-native Taser.js application to Cloudflare Workers, Vercel, AWS Lambda, Bun, Deno, Netlify, or containerized Node servers with zero code changes. ### Install Nitro [#install-nitro] ```bash pnpm add -D nitro ``` ```bash npm install -D nitro ``` ```bash bun add -d nitro ``` ```bash yarn add -D nitro ``` ### Update `vite.config.ts` and Create `nitro.config.ts` [#update-viteconfigts-and-create-nitroconfigts] Chain `nitro()` alongside `taser()` in `vite.config.ts`: ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [taser(), nitro()], }); ``` Create `nitro.config.ts` to declare your target platform preset: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "node-server", // or "cloudflare-module", "vercel", "aws-lambda", "bun", "deno-server" }); ``` ### Build and Run [#build-and-run] ```bash pnpm dev # Vite dev server with instant HMR pnpm build # Compiles production bundle to .output/ node .output/server/index.mjs # Starts production server ``` *** ## 3. Host Pass-Through (Coexisting with Existing Frameworks) [#3-host-pass-through-coexisting-with-existing-frameworks] Host Pass-Through is not a separate setup mode. It is a dispatch mechanism that works seamlessly with **both Vite Standalone and Vite + Nitro**. If you have existing controllers or routes in a Web Standard framework (Hono, Elysia, HatTip, or custom fetch handlers) or a Node.js framework (Express, Fastify), you can keep them running alongside Taser.js: 1. **Taser.js First**: Inbound requests are checked against `src/routes/`. 2. **Host Pass-Through**: If no Taser.js file route matches, the request falls through directly to your exported host application. 3. **404 Handling**: If neither Taser.js nor the host handles the request, Taser.js returns a standard 404 response. Create `src/server.ts` exporting your Fetch-native host app (e.g. Hono, Elysia, or custom fetch handler): ```ts title="src/server.ts" import { Hono } from "hono"; const app = new Hono(); // Legacy or host endpoints: app.get("/host-status", (c) => c.json({ framework: "Hono", status: "online" })); export default app; ``` Install `express` and `srvx`, then create `src/server.node.ts`: ```ts title="src/server.node.ts" import express from "express"; const app = express(); app.use(express.json()); // Legacy Express routes: app.get("/legacy-express", (_req, res) => { res.json({ message: "Dispatched via Express host pass-through" }); }); export default app; ``` Install `fastify` and `srvx`, then create `src/server.node.ts`: ```ts title="src/server.node.ts" import Fastify from "fastify"; const app = Fastify(); // Legacy Fastify routes: app.get("/legacy-fastify", async () => { return { message: "Dispatched via Fastify host pass-through" }; }); await app.ready(); export default app.routing; ``` *** ## Next Steps [#next-steps] # Migration Guide Canonical URL: https://taserjs.dev/docs/getting-started/migration Description: Incrementally migrate existing Express or Fastify APIs to Taser.js with zero downtime using the host pass-through architecture. Taser.js is engineered for **incremental, zero-downtime adoption**. Using the **Host Pass-Through Architecture**, you do not need to rewrite your application all at once. Your existing Express or Fastify server continues running legacy controllers and middlewares, while all newly migrated routes benefit from Taser's filesystem routing, type inference, and response contracts. *** ## Migration Architecture [#migration-architecture] When embedded inside an existing Express or Fastify project, Taser.js sits in front of your legacy server: ``` [Incoming HTTP Request] │ ▼ ┌──────────────────────────────────────┐ │ Taser.js File Routes (src/routes/) │ └──────────────────────────────────────┘ │ Route Match? ├── YES ──► [Execute Taser.js Handler & Middleware] │ └── NO ──► ┌──────────────────────────────────────┐ │ Host Application (src/server.node.ts)│ │ Express / Fastify Routes & Plugins │ └──────────────────────────────────────┘ │ Route Match? ├── YES ──► [Execute Legacy Controller] └── NO ──► [Return 404 Not Found] ``` 1. **Taser.js Evaluates First**: Requests matching files in `src/routes/` are processed with full type safety. 2. **Host Server Fallback**: Any unmatched route immediately falls through to your existing Express or Fastify application. 3. **404 Handling**: If neither Taser.js nor your host application handles the URL, a standard 404 response is returned. *** ## Concept Translation Reference [#concept-translation-reference] Use this literal lookup table to translate familiar Express and Fastify paradigms into their Taser.js equivalents: | Architectural Concept | Express | Fastify | Taser.js Equivalent | | :--------------------- | :-------------------------------- | :---------------------------------------------------- | :--------------------------------------------------------------------------- | | **Route Declaration** | `app.get("/users/:id", fn)` | `fastify.get("/users/:id", fn)` | File: `src/routes/users/$id.get.ts`
Builder: `t.get("/users/:id")` | | **Path Parameters** | `req.params.id` | `request.params.id` | `req.params.id`
(inferred as string or validated via `.params(schema)`) | | **Query Parameters** | `req.query.page` | `request.query.page` | `req.query.page`
(validated and coerced via `.query(schema)`) | | **Request Payload** | `req.body` | `request.body` | `req.body`
(validated via `.body(schema)`) | | **JSON Response** | `res.json(data)` | `reply.send(data)` | `return json(data)` from `@taserjs/router/reply` | | **HTTP Status Code** | `res.status(201).json(data)` | `reply.code(201).send(data)` | `return created(data)` or `return json(data, { status: 201 })` | | **Error Handling** | `res.status(404).json({ error })` | `reply.code(404).send({ error })` | `return notFound({ error })` or `return badRequest(...)` | | **Request Headers** | `req.headers["authorization"]` | `request.headers["authorization"]` | `req.headers.get("authorization")` | | **Cookie Jar** | `req.cookies["token"]` | `request.cookies["token"]` | Mount `cookie()` and use `{ cookies }.get("token")` | | **Middleware Context** | `req.user = user` | `request.user = user` | `return next({ user })` (available as `state.user` in handlers) | | **Global Middleware** | `app.use(cors())` | `fastify.register(cors)` | Root layout: `src/routes/$.ts` | | **Scoped Middleware** | `app.use("/admin", authMw)` | `fastify.register(adminRoutes, { prefix: "/admin" })` | Folder layout: `src/routes/admin.ts` | *** ## Route File Conversion [#route-file-conversion] ### Express to Taser.js [#express-to-taserjs] The following example demonstrates converting an existing Express controller file with route-level parameter validation into a Taser.js file-based route: ```ts title="legacy/routes/users.ts" import { Router, Request, Response } from "express"; import { z } from "zod"; import { db } from "../database"; const router = Router(); const paramsSchema = z.object({ id: z.string().uuid(), }); router.get("/users/:id", async (req: Request, res: Response) => { const parsed = paramsSchema.safeParse(req.params); if (!parsed.success) { return res.status(400).json({ error: parsed.error.issues }); } const user = await db.users.findUnique({ where: { id: parsed.data.id } }); if (!user) { return res.status(404).json({ message: "User not found" }); } return res.json(user); }); export default router; ``` ```ts title="src/routes/users/$id.get.ts" import { json, notFound } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .get("/users/:id") .params(z.object({ id: z.string().uuid() })) .handler(async ({ ctx, req }) => { // req.params.id is strictly validated as a UUID string const user = await ctx.db.users.findUnique({ where: { id: req.params.id } }); if (!user) { return notFound({ message: "User not found" }); } return json(user); }); ``` *The path string above is automatically populated by the dev watcher, production builds (`vite build`, `next build`), or `@taserjs/cli generate` when the file is first created—you don't need to author it by hand even during manual migration.* *** ### Fastify to Taser.js [#fastify-to-taserjs] The following example demonstrates converting an existing Fastify endpoint with JSON body and URL param validation: ```ts title="legacy/routes/items.ts" import { FastifyPluginAsync } from "fastify"; import { z } from "zod"; const itemRoutes: FastifyPluginAsync = async (fastify) => { fastify.post( "/items/:id", { schema: { params: { type: "object", properties: { id: { type: "string" } }, required: ["id"], }, body: { type: "object", properties: { title: { type: "string" }, price: { type: "number" }, }, required: ["title", "price"], }, }, }, async (request, reply) => { const { id } = request.params as { id: string }; const { title, price } = request.body as { title: string; price: number }; const createdItem = await fastify.db.saveItem({ id, title, price }); return reply.code(201).send(createdItem); }, ); }; export default itemRoutes; ``` ```ts title="src/routes/items/$id.post.ts" import { created } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .post("/items/:id") .params(z.object({ id: z.string() })) .body( z.object({ title: z.string(), price: z.number().positive() }), ) .handler(async ({ ctx, req }) => { // req.params.id and req.body are fully typed and validated const createdItem = await ctx.db.saveItem({ id: req.params.id, title: req.body.title, price: req.body.price, }); return created(createdItem); }); ``` *The path string above is automatically populated by the dev watcher, production builds (`vite build`, `next build`), or `@taserjs/cli generate` when the file is first created—you don't need to author it by hand even during manual migration.* *** ## Mapping Middleware to Layout Files [#mapping-middleware-to-layout-files] In Express and Fastify, middlewares are attached imperatively via `.use()` or router scopes. In Taser.js, middleware pipelines are organized declaratively via **layout files** that mirror your route hierarchy: | Scope | Legacy Pattern | Taser.js Layout File | Execution Order | | :-------------------- | :----------------------------------- | :----------------------------- | :--------------------------------------------- | | **Root (Global)** | `app.use(authMiddleware)` | `src/routes/$.ts` | Runs before every route in the project | | **Route Group** | `app.use("/admin", adminAuth)` | `src/routes/admin.ts` | Runs before any route under `/admin/*` | | **Dynamic Parameter** | `app.use("/orgs/:orgId", verifyOrg)` | `src/routes/orgs/$orgId.ts` | Runs before any route under `/orgs/:orgId/*` | | **Pathless Group** | Custom sub-router grouping | `src/routes/_authenticated.ts` | Scopes middleware without adding a URL segment | ### Before / After: Converting Authentication Middleware [#before--after-converting-authentication-middleware] In legacy Express, middleware typically mutates the `req` object (`req.user = user`), which lacks end-to-end type safety: ```ts title="legacy/middleware/auth.ts" import { Request, Response, NextFunction } from "express"; // Requires declaration merging to patch Express.Request declare global { namespace Express { interface Request { user?: { id: string; role: string }; } } } export async function requireAuth(req: Request, res: Response, next: NextFunction) { const token = req.headers.authorization?.replace("Bearer ", ""); if (!token) { return res.status(401).json({ error: "Unauthorized" }); } const user = await verifyToken(token); if (!user) { return res.status(401).json({ error: "Invalid token" }); } req.user = user; next(); } ``` ```ts title="src/routes/admin.ts" import { unauthorized } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; import { verifyToken } from "../lib/auth"; export default t.layout("/admin/*").use(async ({ ctx, req }, next) => { const authHeader = req.headers.get("authorization"); const token = authHeader?.replace("Bearer ", ""); if (!token) { return unauthorized({ error: "Unauthorized" }); } const user = await verifyToken(token); if (!user) { return unauthorized({ error: "Invalid token" }); } // Injects strictly typed user into state for all child routes under /admin/* return next({ user }); }); ``` Any child route inside `src/routes/admin/...` automatically receives `state.user` with full TypeScript inference and zero manual type assertion. *** ## Configuring Host Pass-Through (`src/server.node.ts`) [#configuring-host-pass-through-srcservernodets] To enable seamless coexistence between Taser.js and your legacy server, configure `src/server.node.ts`: ```ts title="src/server.node.ts" import express from "express"; import legacyRoutes from "../legacy/routes/index.js"; const app = express(); // Standard Express middlewares: app.use(express.json()); // Mount un-migrated legacy Express routes: app.use("/api/legacy", legacyRoutes); // Export the Express application instance as the default export export default app; ``` ```ts title="src/server.node.ts" import Fastify from "fastify"; import legacyPlugin from "../legacy/plugins/index.js"; const app = Fastify({ logger: true }); // Mount un-migrated legacy Fastify plugins & routes: await app.register(legacyPlugin, { prefix: "/api/legacy" }); // Ensure Fastify plugins are loaded before export: await app.ready(); // Export Fastify's routing handler as default export export default app.routing; ``` *** ## Known Limitations & Manual Steps [#known-limitations--manual-steps] Certain patterns common in legacy Node.js frameworks cannot be migrated automatically and require intentional restructuring: | Legacy Pattern | Why It Cannot Be Automated | Recommended Migration Strategy | | :----------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------------------------------------------- | :--------------------------------------------------------------------------------------------------------------- | | **Callback-Style Middleware `(req, res, next)`** | Taser.js uses Web Standard Promise-based `(ctx, next) => Promise` architecture; Express callbacks calling `res.end()` bypass the pipeline. | Convert to async functions returning `next({ ... })`, or adapt Hono/Web middleware using `honoMw()`. | | **Imperative Route Loops (`routes.forEach(...)`)** | Taser.js discovers routes statically at build time from the filesystem. | Create explicit `.ts` files per route (or use `@taserjs/cli generate` to scaffold empty route files). | | **Raw Node Stream Piping (`stream.pipe(res)`)** | Node.js `WritableStream` (`res`) is absent in Web Standard environments. | Return a Web Standard `Response` using `pipe()`, `buffer()`, `blob()`, or `sse()` from `@taserjs/router/stream`. | | **Arbitrary Mutating Property Assignment (`req.foo = bar`)** | Mutating global request objects breaks compile-time type safety. | Return state updates via `return next({ foo: bar })` in layout middleware, exposing them on `state` in handlers. | | **WebSocket & HTTP Upgrade Handlers** | Taser.js file routes handle standard HTTP request/response lifecycles. | Keep WebSocket upgrade hooks (`server.on("upgrade", ...)`) inside `src/server.node.ts` or deployment presets. | | **Direct Socket Access (`req.socket`)** | Web Standard `Request` abstracts away raw TCP sockets for multi-runtime edge compatibility. | Extract IP address, TLS info, or socket metadata inside `src/context.ts` or custom middleware adapters. | *** ## Next Steps [#next-steps] # Next.js App Router Canonical URL: https://taserjs.dev/docs/fullstack/nextjs Description: Build fullstack Next.js 15+ App Router applications with a dedicated Taser.js REST API subsystem. Type-safe data fetching in React Server Components, Server Actions, and Client Components. **Next.js App Router** provides a React framework for UI rendering, server components, and streaming HTML. By integrating `@taserjs/plugin/next`, you can embed a structured, file-based REST API subsystem with cascading directory middleware, Standard Schema validation, and compile-time return contracts under `/api`. *** ## Why Pair Taser.js with Next.js? [#why-pair-taserjs-with-nextjs] *** ## Installation [#installation] Install the necessary dependencies in your Next.js project: ```bash pnpm add @taserjs/router @taserjs/client zod pnpm add -D @taserjs/plugin ``` ```bash npm install @taserjs/router @taserjs/client zod npm install -D @taserjs/plugin ``` ```bash bun add @taserjs/router @taserjs/client zod bun add -d @taserjs/plugin ``` ```bash yarn add @taserjs/router @taserjs/client zod yarn add -D @taserjs/plugin ``` *** ## Integration Steps [#integration-steps] ### 1. Wrap `next.config.ts` [#1-wrap-nextconfigts] Use `createTaser` (or `withTaser`) from `@taserjs/plugin/next` to wrap your Next.js configuration: ```ts title="next.config.ts" import type { NextConfig } from "next"; import { createTaser } from "@taserjs/plugin/next"; const nextConfig: NextConfig = { reactStrictMode: true, }; const withTaser = createTaser(); export default withTaser(nextConfig); ``` Put `serverDir` and related paths in `taserjs.config.ts`. Mount the API prefix with `defineTaser().basePath("/api")` in `src/server/taser.ts`. ### 2. Configure TypeScript Paths (`tsconfig.json`) [#2-configure-typescript-paths-tsconfigjson] Configure `@/*` path aliases so Next.js and your IDE resolve `src/` (including `src/server/.taserjs/`): ```json title="tsconfig.json" { "compilerOptions": { "paths": { "@/*": ["./src/*"] } }, "include": ["next-env.d.ts", "src/**/*", "**/*.ts", "**/*.tsx"] } ``` ### 3. Create Router Instance & Context [#3-create-router-instance--context] Create `src/server/context.ts` to manage application boot and request-scoped state: ```ts title="src/server/context.ts" import { createContext } from "@taserjs/router"; export const context = createContext({ boot: () => ({ logger: console, }), request: () => ({ requestId: crypto.randomUUID(), }), }); ``` Create `src/server/taser.ts` with `defineTaser()` (uninstantiated definition; the compiled `app` lives in `src/server/.taserjs/routes.gen.ts`): ```ts title="src/server/taser.ts" import { defineTaser } from "@taserjs/router"; import { notFound } from "@taserjs/router/reply"; import { context } from "./context"; export default defineTaser({ response: { validate: true }, }) .basePath("/api") .context(context) .notFound(() => notFound({ message: "Not Found" })); ``` ### 4. Create Catch-All Route Handler [#4-create-catch-all-route-handler] Create `src/app/api/[[...slug]]/route.ts` (or `app/api/[[...slug]]/route.ts`). This forwards incoming Next.js API requests directly to Taser.js's compiled entry module: ```ts title="src/app/api/[[...slug]]/route.ts" import { app } from "@/server/.taserjs/routes.gen"; const handle = (request: Request) => app.fetch(request); export const GET = handle; export const POST = handle; export const PUT = handle; export const DELETE = handle; export const PATCH = handle; export const OPTIONS = handle; export const HEAD = handle; ``` ### 5. Define File-Based REST Routes [#5-define-file-based-rest-routes] Create your endpoints in `src/server/routes/`: ```ts title="src/server/routes/users.get.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const GET = t .get("/users") .query( z.object({ limit: z.coerce.number().default(10), }), ) .returns({ 200: z.object({ users: z.array( z.object({ id: z.string(), name: z.string(), email: z.string(), }), ), total: z.number(), }), }); export type RouteContext = Parameters[0]; export default GET.handler(({ ctx }) => { return json({ users: [ { id: "usr_1", name: "Alice", email: "alice@example.com" }, { id: "usr_2", name: "Bob", email: "bob@example.com" }, ], total: 2, }); }); ``` ### 6. Create Typed Client Singleton [#6-create-typed-client-singleton] Create `src/client.ts` to export a pre-configured `@taserjs/client` instance: ```ts title="src/client.ts" import { createClient } from "@taserjs/client"; import type { AppManifest } from "@/server/.taserjs/routes.gen"; export const api = createClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL ?? "http://localhost:3000/api", }); ``` *** ## Project Structure Overview [#project-structure-overview] Here is the recommended file layout for a fullstack Next.js App Router project with Taser.js: ``` my-next-app/ ├── next.config.ts # createTaser() / withTaser(nextConfig) ├── taserjs.config.ts # serverDir, routesDir, outputDir, app ├── tsconfig.json # paths: @/* covering src/** including src/server/.taserjs ├── package.json └── src/ ├── app/ # Next.js App Router (React UI & Pages) │ ├── layout.tsx # Root HTML shell │ ├── page.tsx # RSC page fetching from Taser.js API │ └── api/ │ └── [[...slug]]/ │ └── route.ts # Catch-all: import { app } from "@/server/.taserjs/routes.gen" ├── server/ # Taser.js REST API Subsystem │ ├── taser.ts # defineTaser().basePath("/api") definition │ ├── context.ts # Boot and request context │ ├── routes/ # File-based REST endpoints │ │ ├── $.ts # Root layout middleware (optional) │ │ └── users.get.ts # GET /api/users endpoint │ └── .taserjs/ │ └── routes.gen.ts # Generated app + AppManifest + ambient types └── client.ts # createClient({ baseUrl: ... }) ``` *** ## Data Fetching & Mutation Patterns [#data-fetching--mutation-patterns] ### 1. React Server Components (RSC) [#1-react-server-components-rsc] Consume your Taser.js endpoints directly on the server inside React Server Components using your typed client: ```tsx title="src/app/page.tsx" import { api } from "@/client"; export const dynamic = "force-dynamic"; export default async function HomePage() { const res = await api.users.$get({ query: { limit: 20 }, }); if (res.status !== 200) { return
Failed to load users.
; } const { users } = await res.json(); return (

User Directory

    {users.map((user) => (
  • {user.name} ({user.email})
  • ))}
); } ``` ### 2. Next.js Server Actions [#2-nextjs-server-actions] Trigger mutations inside Server Actions and revalidate page paths with complete return shape safety: ```ts title="src/app/actions/create-user.ts" "use server"; import { revalidatePath } from "next/cache"; import { api } from "@/client"; export async function createUserAction(formData: FormData) { const name = formData.get("name") as string; const email = formData.get("email") as string; const res = await api.users.$post({ body: { name, email }, }); if (res.status === 201) { revalidatePath("/"); return { success: true }; } return { success: false, error: "Failed to create user" }; } ``` ### 3. Client Components & React Hooks [#3-client-components--react-hooks] Use Taser.js client calls with TanStack Query or SWR inside Client Components: ```tsx title="src/app/components/user-list.tsx" "use client"; import { useQuery } from "@tanstack/react-query"; import { api } from "@/client"; export function UserList() { const { data, isLoading } = useQuery({ queryKey: ["users"], queryFn: async () => { const res = await api.users.$get({ query: { limit: 10 } }); if (res.status === 200) { return await res.json(); } throw new Error("Failed to fetch users"); }, }); if (isLoading) return
Loading users...
; return (
{data?.users.map((u) => (
{u.name}
))}
); } ``` *** ## Architecture: React Server Components (RSC) vs Taser.js API [#architecture-react-server-components-rsc-vs-taserjs-api] When pairing Taser.js with Next.js App Router, understanding the division of responsibilities ensures clean system design: | Responsibility | Next.js App Router (RSC & Pages) | Taser.js Subsystem (`src/server/`) | | :--------------------- | :--------------------------------------------------------------- | :--------------------------------------------------------------- | | **Primary Focus** | Server-rendered UI, HTML streaming, metadata, layout composition | Structured REST API, JSON endpoints, binary streams, webhooks | | **Execution Context** | React Server Components & Client Components (`src/app/**/*.tsx`) | Type-safe route handlers (`src/server/routes/**/*.ts`) | | **Consumers** | Web browser page visits | Mobile apps, SPA client hooks, webhooks, external developer APIs | | **State & Middleware** | Next.js Edge middleware (`middleware.ts`) | Directory layout middleware pipelines (`src/server/routes/$.ts`) | | **Validation** | Manual or form-action validation | Standard Schema validation (Zod, ArkType, Valibot) | | **Response Safety** | React component props | Compile-time `.returns()` contracts & typed success responses | *** ## Next Steps [#next-steps] # TanStack Start Canonical URL: https://taserjs.dev/docs/fullstack/tanstack-start Description: Build fullstack React applications with TanStack Start and Taser.js. Type-safe data fetching in TanStack Router loaders, React Query, and multi-runtime Nitro deployment. **TanStack Start** is a fullstack React framework powered by TanStack Router and Vite. By integrating `@taserjs/plugin/vite`, you can run a dedicated, high-performance file-based REST API subsystem alongside your TanStack Router UI pages under a custom path prefix (such as `/api`). *** ## Why Pair Taser.js with TanStack Start? [#why-pair-taserjs-with-tanstack-start] *** ## Installation [#installation] Install the necessary dependencies in your TanStack Start project: ```bash pnpm add @taserjs/router @taserjs/client zod pnpm add -D @taserjs/plugin ``` ```bash npm install @taserjs/router @taserjs/client zod npm install -D @taserjs/plugin ``` ```bash bun add @taserjs/router @taserjs/client zod bun add -D @taserjs/plugin ``` ```bash yarn add @taserjs/router @taserjs/client zod yarn add -D @taserjs/plugin ``` *** ## Integration Steps [#integration-steps] ### 1. Configure Vite Plugin [#1-configure-vite-plugin] In `vite.config.ts`, add `taser()` before `tanstackStart()`. Set `server: false` to allow TanStack Start to manage the outer HTTP host lifecycle: ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [ taser({ server: false, // Host pass-through mode config: "./taserjs.config.ts", }), tanstackStart(), viteReact(), ], }); ``` Put `serverDir` in `taserjs.config.ts`. Mount the API with `defineTaser().basePath("/api")`. ### 2. Configure TypeScript (`tsconfig.json`) [#2-configure-typescript-tsconfigjson] Update `tsconfig.json` so TypeScript includes `src/**/*` (covers `src/server/.taserjs/routes.gen.ts`): ```json title="tsconfig.json" { "compilerOptions": { "strict": true, "skipLibCheck": true }, "include": ["src/**/*", "vite.config.ts"] } ``` ### 3. Create Taser.js Router Instance [#3-create-taserjs-router-instance] Create `src/server/taser.ts` with `defineTaser()`: ```ts title="src/server/taser.ts" import { defineTaser } from "@taserjs/router"; export default defineTaser({ response: { validate: true }, }).notFound(() => new Response("Not Found", { status: 404 })); ``` ### 4. Create TanStack Start Catch-All Server Route [#4-create-tanstack-start-catch-all-server-route] Create `src/routes/api/$.tsx`. Dispatch `/api/*` requests to the generated Taser.js `app`: ```tsx title="src/routes/api/$.tsx" import { createFileRoute } from "@tanstack/react-router"; import { app } from "../../server/.taserjs/routes.gen"; const handle = async ({ request }: { request: Request }) => app.fetch(request); export const Route = createFileRoute("/api/$")({ server: { handlers: { GET: handle, POST: handle, PUT: handle, DELETE: handle, PATCH: handle, OPTIONS: handle, HEAD: handle, }, }, }); ``` ### 5. Define File-Based REST Routes [#5-define-file-based-rest-routes] Create your REST endpoints in `src/server/routes/`: ```ts title="src/server/routes/users.get.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const GET = t .get("/users") .query( z.object({ limit: z.coerce.number().default(10), }), ) .returns({ 200: z.object({ users: z.array( z.object({ id: z.string(), name: z.string(), email: z.string(), }), ), total: z.number(), }), }); export type RouteContext = Parameters[0]; export default GET.handler(async ({ ctx }) => { return json({ users: [ { id: "usr_1", name: "Alice", email: "alice@example.com" }, { id: "usr_2", name: "Bob", email: "bob@example.com" }, ], total: 2, }); }); ``` *** ## Project Structure Overview [#project-structure-overview] Here is the recommended layout for a TanStack Start project with Taser.js: ``` my-tanstack-app/ ├── vite.config.ts ├── nitro.config.ts ├── package.json ├── tsconfig.json └── src/ ├── routes/ # TanStack Router UI │ ├── \_\_root.tsx │ ├── index.tsx │ ├── users.tsx │ └── api/ │ └── $.tsx # import { app } from "../../server/.taserjs/routes.gen" └── server/ # Taser.js REST API ├── taser.ts # defineTaser() definition ├── context.ts ├── routes/ │ ├── $.ts │ └── users.get.ts └── .taserjs/ └── routes.gen.ts # Generated app + AppManifest ``` *** ## Adding Nitro for Multi-Platform Deployment [#adding-nitro-for-multi-platform-deployment] TanStack Start utilizes Nitro under the hood for server builds and deployment packaging. You can explicitly include `nitro()` in `vite.config.ts` or add a `nitro.config.ts` file to target edge and serverless environments: ```ts title="vite.config.ts (with Nitro)" import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { tanstackStart } from "@tanstack/react-start/plugin/vite"; import viteReact from "@vitejs/plugin-react"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [ taser({ server: false, config: "./taserjs.config.ts", }), tanstackStart(), viteReact(), nitro(), ], }); ``` Configure your deployment target in `nitro.config.ts`: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; export default defineConfig({ preset: "cloudflare-module", // Target Cloudflare Workers, Vercel, Node, etc. }); ``` *** ## Data Fetching Patterns [#data-fetching-patterns] ### In TanStack Router Loaders [#in-tanstack-router-loaders] Fetch typed data on the server during route transitions with `@taserjs/client`: ```tsx title="src/routes/users.tsx" import { createFileRoute } from "@tanstack/react-router"; import { createClient } from "@taserjs/client"; import type { AppManifest } from "../../server/.taserjs/routes.gen"; const api = createClient({ baseUrl: "http://localhost:3000/api" }); export const Route = createFileRoute("/users")({ loader: async () => { const res = await api.users.$get({ query: { limit: 20 } }); if (res.status !== 200) { throw new Error("Failed to load user directory"); } // res.json() typed from handler reply helpers or optional .returns() contract const data = await res.json(); return { users: data.users }; }, component: UsersPage, }); function UsersPage() { const { users } = Route.useLoaderData(); return (

User Directory

    {users.map((user) => (
  • {user.name} — {user.email}
  • ))}
); } ``` ### With TanStack Query (`@tanstack/react-query`) [#with-tanstack-query-tanstackreact-query] Use Taser.js client calls directly inside `queryOptions` or `useQuery` hooks: ```tsx title="src/hooks/use-users.ts" import { useQuery } from "@tanstack/react-query"; import { createClient } from "@taserjs/client"; import type { AppManifest } from "../../server/.taserjs/routes.gen"; const api = createClient({ baseUrl: "/api" }); export function useUsers(limit = 10) { return useQuery({ queryKey: ["users", limit], queryFn: async () => { const res = await api.users.$get({ query: { limit } }); if (res.status === 200) { return await res.json(); } throw new Error(`API returned error status ${res.status}`); }, }); } ``` *** ## Architecture: TanStack `createServerFn` vs Taser.js REST [#architecture-tanstack-createserverfn-vs-taserjs-rest] TanStack Start provides `createServerFn` for server RPC, while Taser.js provides full file-based REST API routing. Here is how to choose between them: | Requirement | TanStack Start `createServerFn` | Taser.js REST Subsystem (`src/server/`) | | :-------------------------- | :---------------------------------------------------- | :---------------------------------------------------------------- | | **Primary Use Case** | Colocated RPC tightly coupled to React UI components | Structured, public, or versioned HTTP REST APIs | | **Routing Model** | Hash/function-based RPC endpoint | TanStack Router-style file routing (`$id`, `$.ts`, `.get.ts`) | | **Directory Middleware** | Middleware composed inline per function | Scoped directory layouts with cascading typed state | | **Clients & Consumers** | TanStack Router loaders, forms, and client components | Web browsers, mobile apps (iOS/Android), webhooks, 3rd-party devs | | **OpenAPI / Documentation** | Internal to application | Easily documented with standard HTTP methods and JSON payloads | | **Response Guarantees** | TypeScript return types of function | Compile-time `.returns()` contracts & typed success payloads | *** ## Next Steps [#next-steps] # CORS Middleware Canonical URL: https://taserjs.dev/docs/middleware/cors Description: Configure Cross-Origin Resource Sharing (CORS) using @taserjs/router/cors with static origins, dynamic domain resolvers, and preflight support. Taser.js includes a high-performance CORS middleware exported from `@taserjs/router/cors` that handles origin matching, preflight `OPTIONS` requests, credentials, and custom headers. *** ## Basic Configuration [#basic-configuration] Attach CORS to your root layout (`src/routes/$.ts`) to enable cross-origin requests across your entire API: ```ts title="src/routes/$.ts" import { cors } from "@taserjs/router/cors"; import { t } from "@taserjs/router"; export default t.layout("/*").use( cors({ origin: ["https://example.com", "https://app.example.com"], allowMethods: ["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"], allowHeaders: ["Content-Type", "Authorization", "X-Requested-With"], exposeHeaders: ["Content-Length", "X-Request-Id"], credentials: true, maxAge: 86400, }), ); ``` *** ## Dynamic Origin Resolution [#dynamic-origin-resolution] If your application supports multiple dynamic domains or customer vanity subdomains, pass a function to `origin`: ```ts title="src/routes/$.ts" import { cors } from "@taserjs/router/cors"; import { t } from "@taserjs/router"; export default t.layout("/*").use( cors({ origin: (origin) => { // Allow localhost in development: if (!origin || origin.includes("localhost")) { return origin; } // Allow any *.example.com subdomain: if (origin.endsWith(".example.com")) { return origin; } // Disallow all other origins: return null; }, credentials: true, }), ); ``` *** *** ## Scoped CORS per Route Group [#scoped-cors-per-route-group] You can apply different CORS policies to different parts of your API by placing the middleware in specific layout files: ``` src/routes/ ├── $.ts -> Global layout ├── public.ts -> cors({ origin: "*" }) for open public API └── internal.ts -> cors({ origin: "https://admin.internal" }) for internal dashboard ``` *** ## Related Guides [#related-guides] # JWT and JWKS Authentication Canonical URL: https://taserjs.dev/docs/middleware/jwt-and-jwk Description: Verify JSON Web Tokens (JWT) and remote JWKS key sets (Auth0, Clerk, Supabase) with built-in typed authentication middleware for Taser.js APIs. Taser.js includes built-in JWT and JWKS authentication middlewares that verify bearer tokens and inject typed claims on the **`state`** facet (`jwtPayload`). *** ## JWT Authentication (`@taserjs/router/middleware/jwt`) [#jwt-authentication-taserjsroutermiddlewarejwt] The `jwt()` middleware extracts the `Authorization: Bearer ` header, verifies the signature using your secret key, and attaches the typed payload via `next({ jwtPayload })`. ```ts title="src/routes/dashboard.ts" import { jwt } from "@taserjs/router/middleware/jwt"; import { t } from "@taserjs/router"; type JwtClaims = { sub: string; email: string; role: "user" | "admin"; }; export default t.layout("/dashboard").use( jwt({ secret: process.env.JWT_SECRET!, alg: "HS256", }), ); ``` ### Accessing Claims in Downstream Routes [#accessing-claims-in-downstream-routes] Any route inside `src/routes/dashboard/` receives `state.jwtPayload` with complete type inference: ```ts title="src/routes/dashboard/profile.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/dashboard/profile").handler(async ({ ctx, state }) => { // 100% typed from JwtClaims: const userId = state.jwtPayload.sub; const userEmail = state.jwtPayload.email; const userRole = state.jwtPayload.role; const user = await ctx.db.getUser(userId); return json(user); }); ``` *** ## JWKS Authentication (`@taserjs/router/middleware/jwk`) [#jwks-authentication-taserjsroutermiddlewarejwk] For auth providers like Auth0, Clerk, Firebase, AWS Cognito, or Supabase, use `jwk()` to verify tokens against remote JSON Web Key Sets: ```ts title="src/routes/api.ts" import { jwk } from "@taserjs/router/middleware/jwk"; import { t } from "@taserjs/router"; type ClerkPayload = { sub: string; iss: string; azp?: string; }; export default t.layout("/api").use( jwk({ jwks_uri: "https://clerk.your-domain.com/.well-known/jwks.json", }), ); ``` *** ## Configuration Options [#configuration-options] ### JWT Options [#jwt-options] ### JWK Options [#jwk-options] *** ## Next Steps [#next-steps] # Security and Utility Middlewares Canonical URL: https://taserjs.dev/docs/middleware/security-and-utilities Description: Discover built-in middlewares for security headers, CSRF defense, body size limits, ETags, Server-Timing, and response compression. Taser.js bundles a suite of production-grade security and performance utility middlewares. *** ## Secure Headers (`@taserjs/router/middleware/secure-headers`) [#secure-headers-taserjsroutermiddlewaresecure-headers] Sets standard HTTP security headers (HSTS, X-Content-Type-Options, X-Frame-Options, Content-Security-Policy): ```ts title="src/routes/$.ts" import { secureHeaders } from "@taserjs/router/middleware/secure-headers"; import { t } from "@taserjs/router"; export default t.layout("/*").use( secureHeaders({ contentSecurityPolicy: { defaultSrc: ["'self'"], scriptSrc: ["'self'", "'unsafe-inline'"], }, strictTransportSecurity: { maxAge: 31536000, includeSubDomains: true, preload: true, }, xFrameOptions: "DENY", xContentTypeOptions: "nosniff", }), ); ``` *** ## Body Size Limits (`@taserjs/router/middleware/body-limit`) [#body-size-limits-taserjsroutermiddlewarebody-limit] Prevents denial-of-service memory exhaustion by enforcing maximum request body limits: ```ts title="src/routes/uploads.ts" import { bodyLimit } from "@taserjs/router/middleware/body-limit"; import { payloadTooLarge } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.layout("/uploads").use( bodyLimit({ maxSize: 10 * 1024 * 1024, // 10MB limit onError: () => { return payloadTooLarge({ message: "Payload too large. Maximum size is 10MB." }); }, }), ); ``` *** ## CSRF Defense (`@taserjs/router/middleware/csrf`) [#csrf-defense-taserjsroutermiddlewarecsrf] Validates incoming request origin against server origins to prevent cross-site request forgery attacks on state-changing requests (POST, PUT, PATCH, DELETE): ```ts title="src/routes/$.ts" import { csrf } from "@taserjs/router/middleware/csrf"; import { t } from "@taserjs/router"; export default t.layout("/*").use( csrf({ origin: ["https://app.example.com"], }), ); ``` *** ## Automatic ETags (`@taserjs/router/middleware/etag`) [#automatic-etags-taserjsroutermiddlewareetag] Calculates cryptographic hash ETags for response bodies and automatically responds with `304 Not Modified` when clients send a matching `If-None-Match` header: ```ts title="src/routes/$.ts" import { etag } from "@taserjs/router/middleware/etag"; import { t } from "@taserjs/router"; export default t.layout("/*").use( etag({ weak: true, }), ); ``` *** ## Server-Timing (`@taserjs/router/middleware/timing`) [#server-timing-taserjsroutermiddlewaretiming] Adds standard `Server-Timing` headers to responses, enabling Chrome DevTools and observability platforms to measure internal route latency: ```ts title="src/routes/$.ts" import { timing } from "@taserjs/router/middleware/timing"; import { t } from "@taserjs/router"; export default t.layout("/*").use(timing()); ``` *** ## Response Compression (`@taserjs/router/middleware/compress`) [#response-compression-taserjsroutermiddlewarecompress] Automatically compresses outgoing HTTP response payloads using `gzip`, `deflate`, or `brotli` based on client `Accept-Encoding`: ```ts title="src/routes/$.ts" import { compress } from "@taserjs/router/middleware/compress"; import { t } from "@taserjs/router"; export default t.layout("/*").use( compress({ threshold: 1024, // Compress responses larger than 1KB }), ); ``` *** ## Summary of Built-in Middlewares [#summary-of-built-in-middlewares] | Middleware Import | Package | Primary Purpose | | :---------------- | :------------------------------------------ | :------------------------------------ | | `cors` | `@taserjs/router/middleware/cors` | Cross-Origin Resource Sharing | | `jwt` | `@taserjs/router/middleware/jwt` | HMAC/RSA Bearer Token Verification | | `jwk` | `@taserjs/router/middleware/jwk` | Remote JSON Web Key Set Auth | | `secureHeaders` | `@taserjs/router/middleware/secure-headers` | CSP, HSTS, X-Frame-Options | | `bodyLimit` | `@taserjs/router/middleware/body-limit` | Memory protection payload limiting | | `csrf` | `@taserjs/router/middleware/csrf` | Cross-Site Request Forgery mitigation | | `etag` | `@taserjs/router/middleware/etag` | 304 Cache validation | | `timing` | `@taserjs/router/middleware/timing` | Server-Timing performance metrics | | `compress` | `@taserjs/router/middleware/compress` | Gzip / Deflate payload compression | | `cookie` | `@taserjs/router/middleware/cookie` | Signed cookies & Cookie Jar Instance | # Bundler Adapters Canonical URL: https://taserjs.dev/docs/plugins/bundlers Description: Use Taser.js with Webpack, Rspack, Rollup, Rolldown, or Esbuild via @taserjs/plugin subpath exports. Taser.js ships the same unplugin implementation for several bundlers. Each adapter exposes a `taser()` factory with the shared [`TaserPluginOptions`](/docs/api-reference/plugin) used by Vite. For new projects, prefer [Vite](/docs/plugins/vite) or [Vite + Nitro](/docs/plugins/nitro). Bundler adapters are for custom build pipelines that do not use Vite. *** ## Installation [#installation] ```bash pnpm add @taserjs/router @taserjs/plugin @taserjs/cli ``` Install the bundler you use as a development dependency (`webpack`, `@rspack/core`, `rollup`, `rolldown`, or `esbuild`). *** ## Usage [#usage] ```ts title="webpack.config.ts" import { taser } from "@taserjs/plugin/webpack"; export default { plugins: [taser()], }; ``` ```ts title="rspack.config.ts" import { taser } from "@taserjs/plugin/rspack"; export default { plugins: [taser()], }; ``` ```ts title="rollup.config.ts" import { taser } from "@taserjs/plugin/rollup"; export default { plugins: [taser()], }; ``` ```ts title="rolldown.config.ts" import { taser } from "@taserjs/plugin/rolldown"; export default { plugins: [taser()], }; ``` ```ts title="esbuild.config.ts" import { taser } from "@taserjs/plugin/esbuild"; import * as esbuild from "esbuild"; await esbuild.build({ plugins: [taser()], // ... }); ``` *** ## Options [#options] Plugin options: `server`, `serverEntry`, `config`, `cwd`, and `standalone`. Path layout (`serverDir`, `routesDir`, `outputDir`, `app`) comes from [`taserjs.config.ts`](/docs/cli). See the [@taserjs/plugin API reference](/docs/api-reference/plugin). *** ## Subpath reference [#subpath-reference] | Import | Bundler | | :------------------------- | :------- | | `@taserjs/plugin/webpack` | Webpack | | `@taserjs/plugin/rspack` | Rspack | | `@taserjs/plugin/rollup` | Rollup | | `@taserjs/plugin/rolldown` | Rolldown | | `@taserjs/plugin/esbuild` | Esbuild | # Next.js Plugin Canonical URL: https://taserjs.dev/docs/plugins/next Description: Configure @taserjs/plugin/next for Next.js 15+ App Router: createTaser with cwd/config, and routes.gen.ts generation. The `@taserjs/plugin/next` package hooks Taser.js into Next.js 15+ App Router builds. It generates `${serverDir}/.taserjs/routes.gen.ts` from `taserjs.config.ts` and wires Webpack/Turbopack so routes stay in sync during `next dev` and `next build`. For RSC patterns and Server Actions recipes, see the [Next.js App Router Fullstack Guide](/docs/fullstack/nextjs). *** ## Installation [#installation] Install `@taserjs/plugin` and `@taserjs/cli` as development dependencies: `bash pnpm add -D @taserjs/plugin @taserjs/cli ` `bash npm install -D @taserjs/plugin @taserjs/cli ` `bash bun add -d @taserjs/plugin @taserjs/cli ` `bash yarn add -D @taserjs/plugin @taserjs/cli ` Put path layout in `taserjs.config.ts`: ```ts title="taserjs.config.ts" import { defineConfig } from "@taserjs/cli"; export default defineConfig({ serverDir: "src/server", routesDir: "routes", outputDir: ".taserjs", app: "taser.ts", }); ``` *** ## Basic Configuration [#basic-configuration] Wrap `next.config.ts` with `createTaser` (or `withTaser`). Plugin options are only `cwd` and `config`: ```ts title="next.config.ts" import type { NextConfig } from "next"; import { createTaser } from "@taserjs/plugin/next"; const nextConfig: NextConfig = { reactStrictMode: true, }; const withTaser = createTaser(); export default withTaser(nextConfig); ``` Optional overrides: ```ts title="next.config.ts" import { createTaser } from "@taserjs/plugin/next"; const withTaser = createTaser({ config: "./taserjs.config.ts", }); export default withTaser({ reactStrictMode: true, }); ``` Set the HTTP mount with `defineTaser().basePath("/api")` in `src/server/taser.ts`, and point the client `baseUrl` at the same prefix (for example `"/api"`). *** ## Plugin Options [#plugin-options] `createTaser` accepts `NextTaserOptions`: `serverDir`, `routesDir`, `outputDir`, `app`, and `extension` come from [`taserjs.config.ts`](/docs/cli). Runtime URL prefixes use `defineTaser().basePath(...)`, not plugin options. *** ## How It Works [#how-it-works] 1. **Watches `${serverDir}/routes/`** during `next dev` and regenerates on change (with scaffolding for empty files). 2. **Writes `${serverDir}/.taserjs/routes.gen.ts`**: compiled `app`, ambient route types, and the composite manifest for `@taserjs/client`. 3. **Hooks Webpack** (and keeps Turbopack in sync via the watcher) so builds always see a fresh manifest. *** ## Next Steps [#next-steps] # Nitro Module Canonical URL: https://taserjs.dev/docs/plugins/nitro Description: Deploy Taser.js with @taserjs/plugin/nitro — standalone or fullstack Nitro handlers across edge and serverless presets. The `@taserjs/plugin/nitro` package connects Taser.js to the Nitro server engine so you can deploy to Cloudflare Workers, Vercel, AWS Lambda, Bun, Deno, or Node with the same route tree. *** ## Setup & Usage [#setup--usage] ### 1. Via Vite (`vite.config.ts`) [#1-via-vite-viteconfigts] Chain `taser()` and `nitro()`: ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [taser(), nitro()], }); ``` Nitro detects the Vite plugin and applies Taser.js lifecycle hooks. Keep paths in `taserjs.config.ts`. ### 2. Standalone `nitro.config.ts` [#2-standalone-nitroconfigts] For a pure Nitro project, register the Nitro module: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; import { taser } from "@taserjs/plugin/nitro"; export default defineConfig({ modules: [taser()], preset: "node-server", }); ``` *** ## Operational Modes [#operational-modes] Controlled by `standalone` on the plugin options (automatically defaults to `false` when a fullstack framework like TanStack Start, Nuxt, Astro, SvelteKit, or Remix is detected, or `true` in a plain standalone Nitro project): ### 1. Standalone Mode (`standalone: true`, Default for plain Nitro) [#1-standalone-mode-standalone-true-default-for-plain-nitro] Taser.js owns request dispatch: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; import { taser } from "@taserjs/plugin/nitro"; export default defineConfig({ modules: [ taser({ standalone: true, }), ], }); ``` ### 2. Fullstack / Module Mode (`standalone: false`, Automatic when Framework Detected) [#2-fullstack--module-mode-standalone-false-automatic-when-framework-detected] Taser.js registers as a Nitro handler alongside existing server routes. Mount the URL prefix with `defineTaser().basePath("/api")` so only matching paths hit Taser.js: ```ts title="nitro.config.ts" import { defineConfig } from "nitro/config"; import { taser } from "@taserjs/plugin/nitro"; export default defineConfig({ modules: [ taser({ standalone: false, config: "./taserjs.config.ts", }), ], }); ``` *** ## Module Options [#module-options] Shared with other `@taserjs/plugin` adapters (`TaserPluginOptions`): `serverDir` and `routesDir` live in [`taserjs.config.ts`](/docs/cli). Runtime URL prefixes use `defineTaser().basePath(...)`; clients use `baseUrl`. *** ## Next Steps [#next-steps] # Vite Plugin Canonical URL: https://taserjs.dev/docs/plugins/vite Description: Integrate Taser.js with Vite using @taserjs/plugin/vite. Manifest generation, HMR, and standalone or Nitro-backed builds. The `@taserjs/plugin/vite` package connects Taser.js file routing to Vite. It regenerates `src/.taserjs/routes.gen.ts` on change, enables HMR for routes, and can run a dedicated HTTP server in development and production. *** ## Installation [#installation] Install the router, plugin, Vite, and the `srvx` server runtime: `bash pnpm add @taserjs/router pnpm add -D @taserjs/plugin @taserjs/cli vite srvx ` `bash npm install @taserjs/router npm install -D @taserjs/plugin @taserjs/cli vite srvx ` `bash bun add @taserjs/router bun add -d @taserjs/plugin @taserjs/cli vite srvx ` `bash yarn add @taserjs/router yarn add -D @taserjs/plugin @taserjs/cli vite srvx ` *** ## Basic Configuration [#basic-configuration] Paths (`serverDir`, `routesDir`, `outputDir`, `app`) belong in `taserjs.config.ts`. The Vite plugin turns generation and the optional HTTP server on: ```ts title="taserjs.config.ts" import { defineConfig } from "@taserjs/cli"; export default defineConfig({ serverDir: "src", routesDir: "routes", outputDir: ".taserjs", app: "taser.ts", }); ``` ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [taser()], }); ``` Mount URL prefixes with `defineTaser().basePath(...)` in your app definition, not in the Vite plugin options. Point `@taserjs/client` at the same prefix with `baseUrl`. *** ## Execution Modes [#execution-modes] ### 1. Standalone Mode (Default) [#1-standalone-mode-default] Without Nitro, Taser.js runs as a self-contained server powered by Vite: * **Development**: Route discovery and HMR with no separate generate step. * **Production**: Optimized server bundle under `dist/`. ```bash vite build node dist/serve.mjs ``` ### 2. Nitro Mode (Vite + Nitro) [#2-nitro-mode-vite--nitro] Pair `taser()` with `nitro()` when Nitro should package the deployment: ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { nitro } from "nitro/vite"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [taser(), nitro()], }); ``` Nitro uses your `nitro.config.ts` preset (Node, Cloudflare Workers, Vercel, AWS Lambda, and so on). *** ## Plugin Options [#plugin-options] ```ts title="vite.config.ts" import { defineConfig } from "vite"; import { taser } from "@taserjs/plugin/vite"; export default defineConfig({ plugins: [ taser({ server: true, serverEntry: "src/server.ts", config: "./taserjs.config.ts", }), ], }); ``` Set `serverDir`, `routesDir`, `outputDir`, and `app` in [`taserjs.config.ts`](/docs/cli). The plugin loads that file (or built-in defaults) during generate and watch. *** ## Generated Manifest [#generated-manifest] During development and builds, the plugin writes `src/.taserjs/routes.gen.ts` (path follows `serverDir` + `outputDir`): compiled `app`, ambient route types, and the composite manifest for `@taserjs/client`. ```json title="tsconfig.json" { "compilerOptions": { "target": "ES2022", "module": "NodeNext", "moduleResolution": "NodeNext", "strict": true }, "include": ["src/**/*", "vite.config.ts"] } ``` *** ## Next Steps [#next-steps] # Cookie Management Canonical URL: https://taserjs.dev/docs/responses/cookies Description: Mount the first-party cookie() middleware to get a Cookie Jar Instance in your handlers. Read, set, sign, and delete HTTP cookies. Cookies are not available by default. Mount the first-party `cookie()` middleware from `@taserjs/router/middleware/cookie` on a root or scoped layout. Handlers under that layout receive a Cookie Jar Instance as `{ cookies }`. *** ## Mount `cookie()` on a Layout [#mount-cookie-on-a-layout] ### Root layout (`src/routes/$.ts`) [#root-layout-srcroutests] ```ts title="src/routes/$.ts" import { cookie } from "@taserjs/router/middleware/cookie"; import { t } from "@taserjs/router"; export default t.layout("/*").use( cookie({ secret: process.env.COOKIE_SECRET || "your-secure-secret-key-32-chars-long", httpOnly: true, sameSite: "Lax", secure: process.env.NODE_ENV === "production", path: "/", }), ); ``` ### Scoped directory layout [#scoped-directory-layout] Mount only where cookies are needed so sibling branches stay free of cookie headers: ```ts title="src/routes/auth.ts" import { cookie } from "@taserjs/router/middleware/cookie"; import { t } from "@taserjs/router"; export default t.layout("/auth/*").use( cookie({ secret: process.env.COOKIE_SECRET!, path: "/auth", }), ); ``` After you mount `cookie()` on a layout, handlers under that layout can destructure `{cookies}`. Without the middleware, cookies are not available on the handler. *** ## Reading Cookies [#reading-cookies] ```ts title="src/routes/me.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/me").handler(({ cookies }) => { const theme = cookies.get("theme") ?? "light"; const allCookies = cookies.get(); // all parsed request cookies return json({ theme, allCookies }); }); ``` *** ## Setting Cookies [#setting-cookies] ```ts title="src/routes/preferences.post.ts" import { ok } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .post("/preferences") .body( z.object({ theme: z.enum(["light", "dark", "system"]), }), ) .handler(({ cookies, req }) => { cookies.set("theme", req.body.theme, { path: "/", maxAge: 60 * 60 * 24 * 365, httpOnly: true, secure: process.env.NODE_ENV === "production", sameSite: "Lax", }); return ok({ success: true }); }); ``` Set-Cookie headers are applied on the response automatically — you do not append headers manually. *** ## Cryptographically Signed Cookies [#cryptographically-signed-cookies] Pass `secret` when constructing `cookie({ secret })`. Signed helpers then use that default: ```ts title="src/routes/auth/login.post.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .post("/auth/login") .body(z.object({ userId: z.string() })) .handler(async ({ cookies, req }) => { await cookies.setSigned("session_user", req.body.userId); return json({ message: "Logged in successfully" }); }); ``` ```ts title="src/routes/auth/session.get.ts" import { json, unauthorized } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/auth/session").handler(async ({ cookies, ctx }) => { const userId = await cookies.getSigned("session_user"); if (!userId) { return unauthorized({ message: "Invalid or expired session cookie" }); } const user = await ctx.db.getUser(userId); return json(user); }); ``` *** ## Deleting Cookies [#deleting-cookies] ```ts title="src/routes/auth/logout.post.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.post("/auth/logout").handler(({ cookies }) => { const previousSession = cookies.delete("session_user", { path: "/" }); return json({ message: "Logged out", previousSession }); }); ``` *** ## Secure Cookie Prefixes (`__Secure-` and `__Host-`) [#secure-cookie-prefixes-__secure--and-__host-] * **`prefix: "secure"`**: Prepends `__Secure-` and enforces `secure: true`. * **`prefix: "host"`**: Prepends `__Host-`, enforces `secure: true`, `path: "/"`, and prohibits `domain`. ```ts cookies.set("session", sessionId, { prefix: "host" }); const sessionId = cookies.get("session", "host"); ``` *** ## `CookieJarOptions` Reference [#cookiejaroptions-reference] Defaults passed to `cookie(options)` merge into every `set` / `setSigned` / `delete` call: *** ## Cookie Jar Methods [#cookie-jar-methods] *** ## Related Guides [#related-guides] # Global Error Handling Canonical URL: https://taserjs.dev/docs/responses/error-handling Description: Catch unhandled runtime crashes with .onError() and customize 404s with .notFound(). Protocol errors (422/415) and response contracts bypass onError. Taser.js configures error handling on the `defineTaser()` builder in `src/taser.ts`. The runnable Hono instance that applies these handlers is compiled inside `src/.taserjs/routes.gen.ts`. *** ## What `.onError()` Receives [#what-onerror-receives] `.onError()` is dedicated to **unhandled runtime server crashes**. It runs only after built-in protocol handlers have already short-circuited: | Error | Default status | Reaches `.onError()`? | | :-------------------------------------------------- | :------------------ | :-------------------------- | | `ValidationError` (input schemas) | 422 | No — built-in JSON envelope | | `ResponseValidationError` (`.returns()` contracts) | 500 | No — built-in JSON envelope | | `UnsupportedMediaTypeError` | 415 | No — built-in JSON envelope | | Thrown `Response` | as thrown | No — returned as-is | | Everything else (domain `Error`, unexpected throws) | your handler or 500 | Yes | To customize **422 validation** or **response-contract** envelopes, catch them in middleware with `try/catch` (see [Handling Validation Errors](/docs/validation/handling-errors)). Do not expect those errors inside `.onError()`. *** ## Centralized `onError` Boundary [#centralized-onerror-boundary] ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; import { internalServerError, notFound } from "@taserjs/router/reply"; import { context } from "./context.js"; export class ResourceNotFoundError extends Error { constructor(public resource: string) { super(`${resource} not found`); this.name = "ResourceNotFoundError"; } } export default defineTaser() .context(context) .onError((error, req) => { // Domain exceptions thrown from handlers / middleware: if (error instanceof ResourceNotFoundError) { return notFound({ status: 404, error: "Not Found", message: error.message, }); } console.error(`[Error on ${req.method} ${req.path}]:`, error); const isProduction = process.env.NODE_ENV === "production"; return internalServerError({ status: 500, error: "Internal Server Error", message: isProduction ? "An unexpected error occurred" : (error as Error).message, }); }); ``` `.onError((error, req) => Response)` receives a `TaserRequest` as the second argument (`path`, `method`, `params`, `query`, `headers`, `url`, `raw`), not the full handler `ctx`. *** ## Customizing 404 Not Found Handling [#customizing-404-not-found-handling] Use `.notFound()` when an incoming request does not match any registered route: ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; import { notFound } from "@taserjs/router/reply"; import { context } from "./context.js"; export default defineTaser() .context(context) .notFound(({ req }) => { return notFound({ status: 404, error: "Not Found", message: `The endpoint ${req.method} ${req.path} does not exist on this server.`, }); }); ``` *** ## Throwing Errors in Route Handlers [#throwing-errors-in-route-handlers] Because `.onError()` catches unhandled domain and runtime exceptions, you can throw from services or handlers without local `try/catch`: ```ts title="src/routes/teams/$id.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; import { ResourceNotFoundError } from "../../errors.js"; export default t.get("/teams/:id").handler(async ({ ctx, req }) => { const team = await ctx.db.findTeam(req.params.id); if (!team) { // Caught by .onError() and mapped to your 404 JSON response: throw new ResourceNotFoundError("Team"); } return json(team); }); ``` *** ## Related Guides [#related-guides] # Reply Helpers Canonical URL: https://taserjs.dev/docs/responses/reply-helpers Description: Send clean, status-discriminated HTTP responses using tree-shakeable reply helpers: json(), ok(), notFound(), and redirect(). Taser.js provides standalone reply helper functions and a unified `reply` object imported from `@taserjs/router/reply` to construct standardized, type-safe HTTP responses. Every helper returns a standard Web `Response` with its HTTP status code embedded into the TypeScript return type (`ReplyOf`). Because each helper is an independent standalone function, only the helpers you use are bundled into your application. *** ## JSON Responses (`json`) [#json-responses-json] The `json()` function serializes data into a JSON string and automatically sets the `Content-Type: application/json` header: ```ts title="src/routes/users.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/users").handler(({ ctx }) => { return json([ { id: "1", name: "Alice" }, { id: "2", name: "Bob" }, ]); }); ``` You can pass an optional second argument to customize the HTTP status code or set additional response headers: ```ts return json( { message: "Created successfully", id: "user_123" }, { status: 201, headers: { "X-Custom-Header": "ProductService", }, }, ); ``` *** ## Plain Text Responses (`text`) [#plain-text-responses-text] Use `text()` to return raw strings with `Content-Type: text/plain; charset=utf-8`: ```ts title="src/routes/robots.txt.get.ts" import { text } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/robots.txt").handler(() => { return text("User-agent: *\nDisallow: /admin/\n"); }); ``` *** ## HTML Responses (`html`) [#html-responses-html] Use `html()` to return HTML documents or markup strings with `Content-Type: text/html; charset=utf-8`: ```ts title="src/routes/index.get.ts" import { html } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/").handler(() => { return html("

Welcome to Taser.js!

"); }); ``` *** ## Empty / No Content Responses (`noContent`) [#empty--no-content-responses-nocontent] For successful operations that do not return a payload (such as `DELETE` endpoints), use `noContent()`. It sets HTTP status `204`: ```ts title="src/routes/items/$id.delete.ts" import { noContent } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.delete("/items/:id").handler(async ({ ctx, req }) => { await ctx.db.deleteItem(req.params.id); return noContent(); }); ``` *** ## Redirects (`redirect`) [#redirects-redirect] Use `redirect()` to send HTTP redirects (default: `302 Found`): ```ts title="src/routes/old-docs.get.ts" import { redirect } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/old-docs").handler(() => { // Permanent 301 redirect: return redirect("/docs/getting-started", { status: 301 }); }); ``` By default, `redirect()` prevents unintended external open redirects. If you need to redirect to external URLs, pass `{ allowExternal: true }`: ```ts return redirect("https://github.com/taserjs/taserjs", { allowExternal: true }); ``` *** ## Status Code Helpers Reference [#status-code-helpers-reference] All reply helpers are exported from `@taserjs/router/reply`: | Helper Function | HTTP Status | Typical Usage | | :----------------------------------- | :--------------------------- | :--------------------------------------- | | `ok(data?, init?)` | `200 OK` | Generic success response | | `created(data?, init?)` | `201 Created` | Resource created successfully | | `accepted(data?, init?)` | `202 Accepted` | Async job accepted for processing | | `noContent(init?)` | `204 No Content` | Success with empty payload | | `redirect(location, init?)` | `302 Found` | Temporary redirection | | `badRequest(data?, init?)` | `400 Bad Request` | Client query or payload error | | `unauthorized(data?, init?)` | `401 Unauthorized` | Missing or invalid authentication token | | `forbidden(data?, init?)` | `403 Forbidden` | Authenticated user lacks permission | | `notFound(data?, init?)` | `404 Not Found` | Resource or route does not exist | | `methodNotAllowed(data?, init?)` | `405 Method Not Allowed` | HTTP method not supported for this route | | `conflict(data?, init?)` | `409 Conflict` | Duplicate key or concurrency conflict | | `payloadTooLarge(data?, init?)` | `413 Payload Too Large` | Request body exceeds maximum size limit | | `unsupportedMediaType(data?, init?)` | `415 Unsupported Media Type` | Request `Content-Type` not accepted | | `unprocessable(data?, init?)` | `422 Unprocessable Entity` | Schema validation error | | `tooManyRequests(data?, init?)` | `429 Too Many Requests` | Rate limit or quota exceeded | | `internalServerError(data?, init?)` | `500 Internal Server Error` | Unhandled exception | ### Example Usage of Error Helpers [#example-usage-of-error-helpers] ```ts title="src/routes/documents/$id.get.ts" import { forbidden, json, notFound, unauthorized } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/documents/:id").handler(async ({ ctx, req, state }) => { const user = state.user; if (!user) { return unauthorized({ message: "Login required" }); } const doc = await ctx.db.findDocument(req.params.id); if (!doc) { return notFound({ message: "Document not found" }); } if (doc.ownerId !== user.id) { return forbidden({ message: "You do not own this document" }); } return json(doc); }); ``` *** ## Setting Cookies [#setting-cookies] Prefer the first-party `cookie()` middleware and `{ cookies }` jar (see [Cookie Management](/docs/responses/cookies)). You can also set `Set-Cookie` manually via response init: ```ts title="src/routes/auth/login.post.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.post("/auth/login").handler(async () => { const sessionToken = await createSession(); return json( { success: true }, { headers: { "Set-Cookie": `session=${sessionToken}; HttpOnly; Secure; SameSite=Strict; Path=/`, }, }, ); }); ``` *** ## Next Steps [#next-steps] # Response Contracts Canonical URL: https://taserjs.dev/docs/responses/response-contracts Description: Enforce compile-time return shape safety and runtime response validation with .returns(). Eliminate response drift between backend and frontend. In standard backend frameworks, `res.json(data)` is unchecked. If a database query changes or a field name is renamed, backend responses drift silently, breaking frontend consumers in production. Taser.js solves this by letting you define **Response Contracts** with `.returns()`. *** ## Declaring Response Contracts [#declaring-response-contracts] Attach a `.returns()` map containing schemas keyed by HTTP status codes: ```ts title="src/routes/profile.get.ts" import { json, notFound } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const UserProfileSchema = z.object({ id: z.string(), username: z.string(), email: z.string().email(), avatarUrl: z.string().url().nullable(), }); const ErrorSchema = z.object({ message: z.string(), }); export default t .get("/profile") .returns({ // [!code highlight] 200: UserProfileSchema, // [!code highlight] 401: ErrorSchema, // [!code highlight] 404: ErrorSchema, // [!code highlight] }) // [!code highlight] .handler(async ({ ctx, state }) => { const user = await ctx.db.getCurrentUser(state.userId); if (!user) { // Validated against 404 schema: return notFound({ message: "Profile not found" }); } // Validated against 200 schema: return json({ id: user.id, username: user.username, email: user.email, avatarUrl: user.avatar, }); }); ``` *** ## Compile-Time Type Checking [#compile-time-type-checking] The TypeScript compiler checks the value passed into `json()` against the schema corresponding to that status code. If you return an incorrect property name or omit a required field, TypeScript generates an immediate build error: ```ts // ❌ TypeScript Error: Property 'email' is missing in type '{ id: string; username: string }' return json({ id: user.id, username: user.username, }); ``` This ensures you can refactor database models and backend services with total confidence that response shapes remain compliant. *** ## Runtime Response Validation [#runtime-response-validation] In addition to compile-time verification, Taser.js can validate outgoing response payloads at runtime. You can configure response validation behavior in `src/taser.ts`: ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; export default defineTaser({ response: { // Enable/disable runtime response validation (default: true) validate: process.env.NODE_ENV !== "production", // Custom hook when outgoing response does not match .returns() schema onValidationFailure: ({ status, issues, data }) => { console.error( `[Response Validation Error] Status ${status} payload did not match contract:`, issues, ); }, }, }); ``` *** ## Client SDK Success Response Typing [#client-sdk-success-response-typing] `@taserjs/client` types `await res.json()` automatically from your route handlers — **you do not need `.returns()` for client type safety**. By default, the client unions successful `ReplyOf` payload types (`200`–`226`) from reply helpers like `json()`, `ok()`, and `created()`. Use `.returns()` when you want compile-time server-side contract enforcement, runtime response validation in dev/staging, or to override client inference with an explicit `200` schema type. | Server setup | Client `await res.json()` type | | :----------------------------- | :--------------------------------------------------- | | Handler only (no `.returns()`) | Auto-inferred from handler `ReplyOf` success returns | | `.returns({ 200: Schema })` | Schema output type for `200` (takes precedence) | See the [Typed Client Guide](/docs/client#response-typing--handling) for path conventions (`_id`, `param`) and per-request options. ### Status Handling with Typed Success Payloads [#status-handling-with-typed-success-payloads] Call parameterized endpoints using property chains like `_id` and the `param` argument: ```ts title="src/components/user-profile.tsx" import { api } from "@/lib/api"; export async function loadUserProfile(userId: string) { const res = await api.users._id.$get({ param: { id: userId }, }); if (res.status === 200) { // res.json() typed from handler or returns[200] schema const profile = await res.json(); console.log("Welcome back,", profile.username); return profile; } if (res.status === 404) { console.error("User missing"); return null; } if (res.status === 401) { window.location.href = "/login"; return null; } return null; } ``` ### Explicit Status Branching [#explicit-status-branching] You can branch on standard HTTP status codes or `res.ok`: ```ts title="src/lib/fetcher.ts" import { api } from "@/lib/api"; export async function fetchProfile(userId: string) { const res = await api.users._id.$get({ param: { id: userId }, }); if (!res.ok) { throw new Error(`Request failed with status ${res.status}`); } // Typed success payload (handler inference or returns[200]) const data = await res.json(); return data; } ``` *** ## TanStack Query & React Integration [#tanstack-query--react-integration] Typed contracts pair seamlessly with data fetching libraries like `@tanstack/react-query`: ```tsx title="src/hooks/use-profile.ts" import { useQuery } from "@tanstack/react-query"; import { api } from "@/lib/api"; export function useProfile(userId: string) { return useQuery({ queryKey: ["profile", userId], queryFn: async () => { const res = await api.users._id.$get({ param: { id: userId }, }); if (res.status === 200) { return await res.json(); } if (res.status === 404) { throw new Error("Profile not found"); } throw new Error(`Failed to fetch profile: status ${res.status}`); }, }); } ``` *** ## Related Guides [#related-guides] # Streaming and Web Payloads Canonical URL: https://taserjs.dev/docs/responses/streaming-and-files Description: Stream Web ReadableStreams, binary buffers, Blobs, and Server-Sent Events with edge-compatible helpers from @taserjs/router/stream. `@taserjs/router/stream` exposes Web Standard helpers — `pipe`, `buffer`, `blob`, and `sse` — that work the same on Node, Bun, Deno, Workers, and other fetch runtimes. There is no filesystem `file()` helper; serve disk content through your host platform or by wrapping a `ReadableStream` / `Blob` yourself. *** ## Piping Readable Streams (`pipe`) [#piping-readable-streams-pipe] Stream dynamically generated data, AI completions, or proxied upstream bodies: ```ts title="src/routes/ai/generate.post.ts" import { pipe } from "@taserjs/router/stream"; import { t } from "@taserjs/router"; export default t.post("/ai/generate").handler(async () => { const encoder = new TextEncoder(); const customStream = new ReadableStream({ async start(controller) { for (const word of ["Streaming", " live", " AI", " response", "..."]) { controller.enqueue(encoder.encode(word)); await new Promise((resolve) => setTimeout(resolve, 200)); } controller.close(); }, }); return pipe(customStream, { headers: { "Content-Type": "text/plain; charset=utf-8", }, }); }); ``` *** ## Binary Payloads (`buffer`) [#binary-payloads-buffer] Return generated images, PDFs, or other byte arrays: ```ts title="src/routes/export/pdf.get.ts" import { buffer } from "@taserjs/router/stream"; import { t } from "@taserjs/router"; export default t.get("/export/pdf").handler(async ({ req }) => { const pdfBytes: Uint8Array = await generateInvoicePdf(req.query); return buffer(pdfBytes, { headers: { "Content-Type": "application/pdf", "Content-Disposition": 'attachment; filename="invoice.pdf"', }, }); }); ``` `buffer()` defaults `Content-Type` to `application/octet-stream` when you omit it. *** ## Blob Payloads (`blob`) [#blob-payloads-blob] Pass a `Blob` (for example from `fetch` or in-memory construction) and preserve its type when present: ```ts title="src/routes/assets/logo.get.ts" import { blob } from "@taserjs/router/stream"; import { t } from "@taserjs/router"; export default t.get("/assets/logo").handler(async () => { const upstream = await fetch("https://cdn.example.com/logo.png"); const logo = await upstream.blob(); return blob(logo, { headers: { "Cache-Control": "public, max-age=86400", }, }); }); ``` *** ## Server-Sent Events (`sse`) [#server-sent-events-sse] Use `sse()` for the W3C Event Stream protocol. It sets `Content-Type: text/event-stream` and `Cache-Control: no-cache` for you: ```ts title="src/routes/events/notifications.get.ts" import { sse } from "@taserjs/router/stream"; import { t } from "@taserjs/router"; export default t.get("/events/notifications").handler(({ req }) => { return sse( async (stream) => { await stream.write({ event: "connected", data: { ok: true } }); const interval = setInterval(() => { void stream.write({ event: "tick", data: { time: new Date().toISOString() }, }); }, 1000); stream.onAbort(() => clearInterval(interval)); }, { signal: req.raw.signal }, ); }); ``` Prefer `sse()` for responses. Import `formatSSE` when you need to encode individual messages into SSE text for a custom `ReadableStream` passed to `pipe()`. *** ## Next Steps [#next-steps] # Context and State Canonical URL: https://taserjs.dev/docs/routing/context-and-state Description: Learn how to manage application singletons and request-scoped state with createContext. Understand boot context, request context, and native runtime interop. Taser.js features a dual-layer dependency injection system created with `createContext()`. It separates long-lived application singletons (boot context) from ephemeral per-request metadata (request context). *** ## Dual-Layer Context Overview [#dual-layer-context-overview] ```ts title="src/context.ts" import { createContext } from "@taserjs/router"; import { PrismaClient } from "@prisma/client"; import { createClient as createRedisClient } from "redis"; export const context = createContext({ // 1. Boot Context: Initialized once when the server starts boot: async () => { const db = new PrismaClient(); await db.$connect(); const redis = createRedisClient(); await redis.connect(); return { db, redis, env: process.env.NODE_ENV ?? "development", }; }, // 2. Request Context: Initialized on every incoming HTTP request request: (req: Request) => { const requestId = req.headers.get("x-request-id") ?? crypto.randomUUID(); const startTime = Date.now(); return { requestId, startTime, }; }, }); ``` Attach your context in `src/taser.ts`: ```ts title="src/taser.ts" import { defineTaser } from "@taserjs/router"; import { context } from "./context.js"; const taser = defineTaser().context(context); export default taser; export type AppContext = typeof taser.$Infer.Context; ``` *** ## Boot Context vs Request Context [#boot-context-vs-request-context] | Feature | Boot Context (`boot`) | Request Context (`request`) | | :---------------- | :------------------------------------------------------------ | :------------------------------------------------------ | | **Execution** | Runs once when the application boots up | Runs once for each incoming request | | **Async Support** | Fully async (`async () => ({ ... })`) | Sync or async (`(req: Request) => ({ ... })`) | | **Best Used For** | Database pools, Redis connections, SDK clients, configuration | Request IDs, timing markers, trace headers, tenancy IDs | | **Performance** | Zero per-request overhead | Lightweight object allocation per request | *** ## Accessing Context in Handlers [#accessing-context-in-handlers] Every route handler and middleware automatically receives the combined properties of `boot` and `request` context directly on the `ctx` object: ```ts title="src/routes/users/$id.get.ts" import { json, notFound } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/users/:id").handler(async ({ ctx, req }) => { // Available from boot context: const user = await ctx.db.user.findUnique({ where: { id: req.params.id }, }); if (!user) { return notFound({ message: "User not found" }); } // Available from request context: const elapsed = Date.now() - ctx.startTime; console.log(`[${ctx.requestId}] User ${user.id} fetched in ${elapsed}ms`); return json(user); }); ``` ### Typing Extracted Helper Functions [#typing-extracted-helper-functions] When writing helper functions that take `ctx` as an argument, extract the context type from the route builder variable (`GET`, `POST`, etc.) before calling `.handler()`: ```ts title="src/routes/users/$id.get.ts" import { json, notFound } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; // 1. Declare builder variable const GET = t.get("/users/:id").params(z.object({ id: z.string() })); // 2. Extract RouteContext from the builder export type RouteContext = Parameters[0]; // 3. Type your helper functions with RouteContext async function getUserProfile({ ctx, req }: RouteContext) { return ctx.db.user.findUnique({ where: { id: req.params.id }, }); } // 4. Call helper inside .handler() export default GET.handler(async (args) => { const user = await getUserProfile(args); if (!user) { return notFound({ message: "User not found" }); } return json(user); }); ``` *** ## Accessing the Standard Request in `createContext` [#accessing-the-standard-request-in-createcontext] The `request` hook receives the standard Web `Request` object for each incoming dispatch: ```ts title="src/context.ts" import { createContext } from "@taserjs/router"; export const context = createContext({ request: (req: Request) => { const userAgent = req.headers.get("user-agent") ?? "unknown"; const clientIp = req.headers.get("x-forwarded-for")?.split(",")[0]?.trim() ?? req.headers.get("x-real-ip") ?? "127.0.0.1"; return { userAgent, clientIp, requestId: crypto.randomUUID(), }; }, }); ``` *** ## Reserved Context Keys [#reserved-context-keys] To avoid collisions with internal routing properties, the following property names are reserved and cannot be returned by `createContext`: * `state` * `query` * `params` * `body` * `headers` * `cookies` * `method` * `path` * `url` * `request` If you attempt to return a reserved key from `createContext()`, TypeScript will generate a compiler error on the `createContext()` call. *** ## Next Steps [#next-steps] # Defining Routes Canonical URL: https://taserjs.dev/docs/routing/defining-routes Description: 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 [#route-builders] The `t` router instance provides fluent builder methods corresponding to all standard HTTP methods: ```ts 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); ``` 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 [#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` | | **Validation Layer** | Build-time duplicate route checks | TypeScript compiler (`tsc`) via ambient `RoutePath` | ### Compile-Time vs. Build-Time Validation [#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.ts` must call `t.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/cli` or the Vite/Next plugin) augment `RouterRegister.RoutePath`. Passing an unknown or misspelled route path causes TypeScript (`tsc`) to report a compile error: ```text 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")` inside `src/routes/articles.get.ts`), runtime routing still dispatches strictly to `/articles`, but `req.params` will drift to reflect `{ id: string }` instead of `{}`. #### Before / After: Aligning Route Path Strings [#before--after-aligning-route-path-strings] ```ts title="src/routes/articles.get.ts (Before - Path Mismatch)" 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 }); }); ``` ```ts title="src/routes/articles.get.ts (After - Aligned Path String)" 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 [#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 [#defining-a-get-route] GET, DELETE, and OPTIONS routes accept validation schemas for `query` and `params`: ```ts title="src/routes/articles.get.ts" 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--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: ```ts title="src/routes/tasks/$id.get.ts" 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 [#defining-a-post-route] POST, PUT, PATCH, and QUERY routes accept `.body()`, `.query()`, and `.params()` schemas: ```ts title="src/routes/articles.post.ts" 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); }); ``` The `.body()` method validates `application/json` by default and also supports `form`, `text`, and `raw` modes. See the [Standard Schema Validation Guide](/docs/validation/standard-schema#validating-multipart-form-data--file-uploads) for examples on validating `File` uploads, sizes, and MIME types. *** ## Route-Level Middlewares [#route-level-middlewares] In addition to directory layout middlewares, you can attach route-specific middleware using `.use()`: ```ts title="src/routes/admin/purge.delete.ts" 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`) [#multi-method-handlers-tany-and-tall] When an endpoint needs to handle multiple HTTP methods with shared logic, use `t.any()` or `t.all()`: ```ts title="src/routes/webhooks/stripe.post.ts" 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 [#related-guides] # File Conventions Canonical URL: https://taserjs.dev/docs/routing/file-conventions Description: Master TanStack Router-style file conventions for REST APIs. Learn flat routes, nested folders, path params ($id), catch-alls ($), and layouts. Taser.js adopts modern **TanStack Router-style file routing conventions** tailored specifically for REST APIs. Your filesystem serves as the single source of truth for URL endpoints, HTTP methods, and cascading middleware pipelines. *** ## Route Files vs Layout Files [#route-files-vs-layout-files] Under your `src/routes/` directory, files are classified into two distinct types: 1. **Route Files**: Filenames containing an HTTP verb before `.ts` (such as `users.get.ts`, `posts.post.ts`, `items.$id.delete.ts`, `tasks/$id.complete.patch.ts`). Route files export a default route definition created with `t.get()`, `t.post()`, etc. 2. **Layout Files**: Filenames ending in `.ts` without an HTTP verb (such as `$.ts`, `admin.ts`, `_auth.ts`, `tasks/$id.ts`). Layout files export a default middleware pipeline created with `t.layout()`. The path string passed to `t.get()`, `t.post()`, `t.layout()`, etc. is generated automatically by the dev watcher (`vite dev`, `next dev`), standard production builds (`vite build`, `next build`), or `@taserjs/cli` when route files are first created (e.g. via `touch src/routes/...`). You do not need to author this string by hand. **Scaffolding & Mismatch Behavior**: Blank files are automatically populated with starter boilerplate and the matching path string, but existing, populated files are never overwritten. If you rename a route file or edit its path string to an invalid path, your code is preserved; instead, TypeScript flags the mismatch at compile time (`Type '"..."' is not assignable to type 'RoutePath'`). **Production Builds vs. Standalone Typechecking**: Standard builds (`vite build`, `next build`) automatically scan routes and write `src/.taserjs/routes.gen.ts` during compilation—no separate CLI command is required in build pipelines. If your setup runs standalone typechecks (`tsc --noEmit`) in CI or pre-commit hooks outside of a bundler build, install `@taserjs/cli` as a dev dependency and add `"typecheck": "taser generate && tsc --noEmit"` to your `package.json` (or invoke on-demand via `npx @taserjs/cli generate`). ### Source of Truth Comparison [#source-of-truth-comparison] | Aspect | Filesystem Path (`src/routes/...`) | Builder String (`t.get("/path")`, `t.layout("/*")`) | | :----------------------- | :------------------------------------------------------ | :------------------------------------------------------------------------- | | **URL Determination** | **Sole source of truth** for URL matching | No runtime impact on request routing | | **AST Scan Validation** | Enforces valid file conventions and factory verb parity | Scanned for factory name (`t.get`, `t.layout`), not path identity | | **TypeScript Typecheck** | Emits ambient `RoutePath` / `LayoutId` types | Validated by `tsc` against ambient `RoutePath`; types `req.params` | | **Mismatch Consequence** | Determines where requests actually route | `tsc` compile error if invalid; param type drift if matching another route | #### Route Path String Mismatch Error [#route-path-string-mismatch-error] If the path string in a route builder does not match any valid route path in the emitted ambient union, TypeScript flags the mismatch during `tsc`: ```text Type '"/usr/profile"' is not assignable to type 'RoutePath'. ``` *** ## HTTP Method Suffixes [#http-method-suffixes] Taser.js determines the HTTP verb from the final extension segment before `.ts`: | File Name | HTTP Method | Endpoint URL | | :---------------------------- | :--------------------- | :----------- | | `src/routes/users.get.ts` | `GET` | `/users` | | `src/routes/users.post.ts` | `POST` | `/users` | | `src/routes/users.put.ts` | `PUT` | `/users` | | `src/routes/users.patch.ts` | `PATCH` | `/users` | | `src/routes/users.delete.ts` | `DELETE` | `/users` | | `src/routes/users.options.ts` | `OPTIONS` | `/users` | | `src/routes/users.query.ts` | `QUERY` | `/users` | | `src/routes/users.any.ts` | `ANY` (multi-method) | `/users` | | `src/routes/users.all.ts` | `ALL` (catch-all verb) | `/users` | *** ## Flat Routes, Directory Routes, and Mixed Notation [#flat-routes-directory-routes-and-mixed-notation] Taser.js supports **nested directory structures**, **flat dot notation**, and **mixed folder + dot notation**. You can use whichever structure keeps your codebase cleanest: A dot `.` inside a filename (e.g. `tasks/$id.complete.patch.ts` or `posts.$id.edit.get.ts`) acts as a URL segment separator (`/`), allowing you to define sub-routes flatly without creating deeply nested folders. *** ## Index Routes (`index..ts`) [#index-routes-indexmethodts] An `index` segment targets the root of its parent route without appending `/index` to the public URL: ### Index Routes vs. Dynamic Parameter Routes (`$param`) [#index-routes-vs-dynamic-parameter-routes-param] | Pattern | Notation | Example File | Resolved URL | Recommended Use Case | | :------------------------------ | :---------------- | :--------------------------------------- | :--------------- | :------------------------------------------------------------------------------------------------------------------------------ | | **Direct Param Route** | Flat or Directory | `posts.$id.get.ts` or `posts/$id.get.ts` | `GET /posts/:id` | **Standard**: Standalone endpoint for a single parameter leaf. | | **Directory Index Param Route** | Directory | `posts/$id/index.get.ts` | `GET /posts/:id` | **Grouping**: Co-locating root parameter handler with nested sub-routes (`posts/$id/comments.get.ts`, `posts/$id/edit.put.ts`). | | **Redundant Flat Index** | Flat Dot | `posts.$id.index.get.ts` | `GET /posts/:id` | **Discouraged**: Redundant syntax in flat dot notation; prefer `posts.$id.get.ts`. | ### Collision & Duplicate Route Rules [#collision--duplicate-route-rules] Both `posts.$id.get.ts` and `posts.$id.index.get.ts` (as well as `posts/$id/index.get.ts`) evaluate to the exact same route: `GET /posts/:id`. If both files exist simultaneously within the same routes directory, Taser.js **halts the build immediately** during the route scan phase with a `ScanErrorCollection`: ```text ScanError: Duplicate route for GET /posts/:id (posts.$id.get.ts and posts.$id.index.get.ts) ``` Route collisions are **never silently overwritten** and do not result in last-write-wins or undefined behavior. You must remove one of the duplicate route files to resolve the build error. *** ## Dynamic Path Parameters (`$param`) [#dynamic-path-parameters-param] Prefixing a segment with `$` denotes a named URL path parameter: Inside your route handler, parameters are automatically typed and validated on `req.params`: ```ts title="src/routes/tasks/$id.complete.patch.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .patch("/tasks/:id/complete") .params(z.object({ id: z.string().uuid() })) .handler(({ req }) => { // req.params.id is strictly typed as a UUID string return json({ taskId: req.params.id, completed: true }); }); ``` In the example above, `src/routes/tasks/$id.complete.patch.ts` is the **standard layout-inheriting** route for `PATCH /tasks/:id/complete`. If a layout file `src/routes/tasks/$id.ts` exists, this route automatically inherits that layout's middleware chain (`$.ts` → `tasks/$id.ts`). See [Layout Breakout Routes](#layout-breakout-routes-trailing-underscore-segment_) for the alternative syntax that skips this layout. *** ## Wildcard Catch-All Splats (`$`) [#wildcard-catch-all-splats-] A standalone `$` segment captures wildcard rest segments (splats): In your handler, the wildcard path matches all trailing sub-paths. *** ## Root Layout Middleware (`$.ts`) [#root-layout-middleware-ts] A top-level file named `src/routes/$.ts` serves as the **Root Layout Middleware**. It runs before every route in your application: ```ts title="src/routes/$.ts" import { bodyLimit } from "@taserjs/router/body-limit"; import { secureHeaders } from "@taserjs/router/secure-headers"; import { t } from "@taserjs/router"; export default t .layout("/*") .use(secureHeaders()) .use(bodyLimit({ maxSize: 1_000_000 })); ``` *** ## Pathless Layouts (Leading Underscore `_layout`) [#pathless-layouts-leading-underscore-_layout] A segment starting with an underscore `_` (like `_auth.ts`, `_auth/`, or `_auth.login.post.ts`) is **pathless**. It applies scoped middleware to child routes without adding any segment to the public URL: In flat dot notation: Notice that `_auth`, `_app`, and `_dashboard` are omitted from the public URL. *** ## Layout Breakout Routes (Trailing Underscore `segment_`) [#layout-breakout-routes-trailing-underscore-segment_] A segment ending with an underscore `_` is a **breakout route** (un-nested route). It targets the expected URL path, but **breaks out of the parent layout hierarchy** so it skips that segment's layout middleware. ### The Problem Breakout Routes Solve [#the-problem-breakout-routes-solve] Suppose you have an authenticated `/posts` layout in `src/routes/posts.ts` (or a `/tasks/:id` layout in `src/routes/tasks/$id.ts`) that enforces user login or permission checks. You want a specific child endpoint (like `/posts/:id/preview` or `/tasks/:id/complete`) to skip that layout's checks. By adding a trailing underscore to `posts_` or `$id_`, the route keeps the URL segment but **skips** the layout: | File | URL Path | Layout Middleware Chain | | :-------------------------- | :----------------------- | :------------------------------------------ | | `posts/index.get.ts` | `GET /posts` | `$.ts` → `posts.ts` | | `posts/$id.get.ts` | `GET /posts/:id` | `$.ts` → `posts.ts` | | `posts_.$id.preview.get.ts` | `GET /posts/:id/preview` | `$.ts` *(Breaks out of posts.ts)* | ### Breakout Route vs. Base Route Alternatives [#breakout-route-vs-base-route-alternatives] The base route `tasks/$id.complete.patch.ts` and the breakout route `tasks/$id_.complete.patch.ts` resolve to the **identical endpoint URL** (`PATCH /tasks/:id/complete`). They are **mutually exclusive architectural alternatives** for the same URL: | Route Configuration | File Location | Resolved Endpoint | Middleware Chain | Purpose | | :------------------------------ | :----------------------------- | :-------------------------- | :---------------------- | :------------------------------------------------------------------------------------- | | **Standard Route (Inheriting)** | `tasks/$id.complete.patch.ts` | `PATCH /tasks/:id/complete` | `$.ts` → `tasks/$id.ts` | Runs the `$id.ts` layout checks (e.g. verifying task ownership or existence). | | **Breakout Route (Skipping)** | `tasks/$id_.complete.patch.ts` | `PATCH /tasks/:id/complete` | `$.ts` only | Bypasses the `tasks/$id.ts` layout checks while retaining the identical URL structure. | These two files **cannot coexist** in the same routes tree. If both `tasks/$id.complete.patch.ts` and `tasks/$id_.complete.patch.ts` are present simultaneously, Taser.js halts compilation with a build error during route scanning: ```text ScanError: Duplicate route for PATCH /tasks/:id/complete (tasks/$id.complete.patch.ts and tasks/$id_.complete.patch.ts) ``` Taser.js enforces strict uniqueness per `[URL + HTTP Method]`. Collisions always result in a **build-time error**, never a silent override or last-write-wins. * **Leading underscore (`_auth`)**: Pathless layout / group (adds middleware, removes segment from URL). - **Trailing underscore (`posts_` or `$id_`)**: Breakout route (keeps segment in URL, removes parent layout from middleware chain). *** ## Escaping Special Characters (`[...]`) [#escaping-special-characters-] When you need a literal character that would otherwise trigger a router convention (such as a literal dot `.`, literal leading underscore `_`, or literal `index`), wrap the character in brackets `[...]`: | File Name | Resolved URL | Explanation | | :---------------------------------- | :------------------- | :--------------------------------------------------------------------- | | `src/routes/sitemap[.]xml.get.ts` | `GET /sitemap.xml` | Escaped dot `[.]` prevents splitting into `/sitemap/xml` | | `src/routes/docs/v1[.]0/api.get.ts` | `GET /docs/v1.0/api` | Preserves `v1.0` as a single path segment | | `src/routes/[_]private.get.ts` | `GET /_private` | Escaped `[_]` prevents segment from being treated as a pathless layout | | `src/routes/tasks/task[_].get.ts` | `GET /tasks/task_` | Escaped `[_]` prevents segment from being treated as a layout breakout | | `src/routes/items/[index].get.ts` | `GET /items/index` | Escaped `[index]` prevents trimming to `/items` | *** ## Ignored & Private Files (`-` Prefix) [#ignored--private-files---prefix] To co-locate utility files, helper functions, schemas, or test fixtures directly alongside route files without generating routes, prefix the file or folder with a dash `-`: *** ## Complete Conventions Reference [#complete-conventions-reference] | File Path in `src/routes/` | HTTP Method | Resolved API URL | Layout Inheritance | Description | | :----------------------------- | :---------- | :-------------------- | :----------------- | :---------------------------------------------- | | `$.ts` | N/A | Global | Root | Root application layout | | `index.get.ts` | `GET` | `/` | `/*` | Root index route | | `posts.ts` | N/A | `/posts` | `/*` | Scoped layout for posts | | `posts.index.get.ts` | `GET` | `/posts` | `/*` → `/posts` | Flat posts index | | `posts.$id.get.ts` | `GET` | `/posts/:id` | `/*` → `/posts` | Flat parameterized route | | `tasks/$id.complete.patch.ts` | `PATCH` | `/tasks/:id/complete` | `/*` → `/tasks` | Mixed folder + dot nested route | | `posts_.$id.edit.get.ts` | `GET` | `/posts/:id/edit` | `/*` | **Breakout route**: skips `posts.ts` layout | | `tasks/$id_.complete.patch.ts` | `PATCH` | `/tasks/:id/complete` | `/*` → `/tasks` | **Breakout route**: skips `tasks/$id.ts` layout | | `files.$.get.ts` | `GET` | `/files/*` | `/*` | Wildcard splat handler | | `_auth.ts` | N/A | Scoped | `/*` | Pathless auth layout | | `_auth.settings.get.ts` | `GET` | `/settings` | `/*` → `/_auth` | Pathless scoped route | | `sitemap[.]xml.get.ts` | `GET` | `/sitemap.xml` | `/*` | Bracket-escaped literal dot | | `-helpers.ts` | N/A | N/A | N/A | Ignored file | *** ## Next Steps [#next-steps] # Layouts and Middleware Canonical URL: https://taserjs.dev/docs/routing/layouts-and-middleware Description: Compose shared logic, authentication guards, and cascading typed state with layout files. Understand middleware execution ordering and state propagation. Layout files in Taser.js allow you to organize cross-cutting concerns (authentication, CORS, rate limiting, logging) and inject typed state into child routes without repeating code in every handler. Ambient layout IDs and hierarchy types are emitted into `src/.taserjs/routes.gen.ts` alongside the compiled `app`, so TypeScript can verify `t.layout(...)` path strings against your filesystem tree. *** ## What is a Layout File? [#what-is-a-layout-file] A layout file is any TypeScript file inside `src/routes/` that does not end with an HTTP method. It exports a default middleware pipeline initialized with `t.layout(layoutId)` or `layout(layoutId)`. ```ts title="src/routes/admin.ts" import { forbidden, unauthorized } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.layout("/admin").use(async ({ ctx, req }, next) => { const authHeader = req.headers.get("authorization"); if (!authHeader?.startsWith("Bearer ")) { return unauthorized({ message: "Admin authorization required" }); } const token = authHeader.slice(7); const adminUser = await verifyAdminToken(token); if (!adminUser) { return forbidden({ message: "Insufficient permissions" }); } // Injects adminUser directly into state for all downstream routes return next({ adminUser }); }); ``` All route files in `src/routes/admin/` (or `src/routes/admin.*.ts`) automatically inherit this middleware and receive `state.adminUser` fully typed. *** ## Root Layout (`src/routes/$.ts`) [#root-layout-srcroutests] The root layout applies to every route in your application. It is the ideal place for global concerns like CORS, security headers, request timing, and tenant resolution: ```ts title="src/routes/$.ts" import { cors } from "@taserjs/router/cors"; import { secureHeaders } from "@taserjs/router/secure-headers"; import { timing } from "@taserjs/router/timing"; import { t } from "@taserjs/router"; export default t .layout("/*") .use( cors({ origin: ["https://app.example.com", "https://admin.example.com"], credentials: true, }), ) .use(secureHeaders()) .use(timing()); ``` *** ## Root Index Layout (`src/routes/index.ts`) [#root-index-layout-srcroutesindexts] While `src/routes/$.ts` applies globally to every route in your application, `src/routes/index.ts` is scoped specifically to root index endpoints (such as `src/routes/index.get.ts`). It mounts with `t.layout('/index')`: ```ts title="src/routes/index.ts" import { t } from "@taserjs/router"; export default t.layout("/index").use(async ({ ctx, req }, next) => { // Only runs for the root '/' endpoint return next(); }); ``` Nested directory index files (e.g. `src/routes/admin/index.ts`) similarly mount with `t.layout('/admin/index')` and execute only for `GET /admin` (`src/routes/admin/index.get.ts`). *** ## Nested Layout Hierarchy & Execution Order [#nested-layout-hierarchy--execution-order] Taser.js executes layouts in a predictable cascading sequence from outermost to innermost: Execution sequence for `GET /api/users`: ``` [1. Root Layout: $.ts] │ ▼ [2. API Layout: api.ts] │ ▼ [3. Pathless Layout: _auth.ts] │ ▼ [4. Role Layout: admin.ts] │ ▼ [5. Route Handler: users.get.ts] ``` *** ## Injecting Typed State [#injecting-typed-state] When middleware calls `next({ session })`, the properties merge into the **`state`** facet. Downstream handlers access them with complete TypeScript inference: ```ts title="src/routes/dashboard.ts" import { unauthorized } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.layout("/dashboard").use(async ({ ctx, req }, next) => { const session = await getSession(req.headers.get("cookie")); if (!session) { return unauthorized({ message: "Invalid session" }); } return next({ session }); }); ``` ```ts title="src/routes/dashboard/metrics.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/dashboard/metrics").handler(async ({ ctx, req, state }) => { // state.session is 100% typed from dashboard.ts: const { userId, workspaceId } = state.session; const metrics = await ctx.db.getWorkspaceMetrics(workspaceId); return json({ userId, metrics }); }); ``` *** ## Standalone Middlewares with `middleware()` [#standalone-middlewares-with-middleware] To create reusable, type-safe middlewares across layout files or projects, use `middleware()` or `t.middleware()`: ```ts title="src/middleware/rate-limit.ts" import { middleware } from "@taserjs/router"; import { badRequest } from "@taserjs/router/reply"; export function rateLimiter(options: { maxRequests: number; windowSeconds: number }) { return middleware(async ({ ctx, req }, next) => { const clientIp = req.headers.get("x-forwarded-for") ?? "127.0.0.1"; const remaining = await checkRateLimit(clientIp, options); if (remaining <= 0) { return badRequest({ message: "Rate limit exceeded" }); } return next({ rateLimitRemaining: remaining }); }); } ``` ### App Context Inheritance in Middlewares [#app-context-inheritance-in-middlewares] Middlewares created via `middleware()` or `t.middleware()` automatically inherit your application's boot and request context (`ctx.db`, `ctx.requestId`, etc.) via ambient type registration with zero manual type annotations: ```ts title="src/middleware/tenant-logger.ts" import { middleware } from "@taserjs/router"; export const tenantLogger = middleware(async ({ ctx, req }, next) => { // ctx.db and ctx.requestId are 100% typed from your taser.context definition! ctx.db.logTenantAccess(ctx.requestId); return next(); }); ``` *** ## Layout-Scoped Middlewares [#layout-scoped-middlewares] When creating middleware tailored for a specific route hierarchy, pass the layout identifier as the first argument to `middleware()`. ### 1. Single Layout Binding [#1-single-layout-binding] Passing a layout identifier binds the middleware directly to that layout branch: ```ts title="src/middleware/user-guard.ts" import { middleware } from "@taserjs/router"; import { forbidden } from "@taserjs/router/reply"; export const requireActiveUser = middleware("/users", async ({ ctx, req, state }, next) => { // 1. state automatically inherits all state provided by the "/users" layout chain: const user = state.user; if (!user.isActive) { return forbidden({ message: "User account suspended" }); } // 2. Injects additional state downstream: return next({ userTier: user.tier }); }); ``` #### Compile-Time Branch Safety [#compile-time-branch-safety] TypeScript guarantees that `requireActiveUser` can only be attached to routes or layouts under the `"/users"` branch: ```ts title="src/routes/users/settings.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; import { requireActiveUser } from "../../middleware/user-guard"; // Allowed: Route inherits "/users" layout export default t .get("/users/settings") .use(requireActiveUser) .handler(({ state }) => { return json({ tier: state.userTier }); }); ``` If you attempt to mount it on an unrelated branch, TypeScript emits an immediate compile error: ```ts title="src/routes/posts/$id.get.ts" import { ok } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; // ❌ TypeScript Error: Cannot attach middleware scoped to "/users" layout on "/posts" branch export default t .get("/posts/:id") .use(requireActiveUser) .handler(() => ok()); ``` *** ## Precondition Requirements (`.requires<{ state?, params?, query?, body? }>()`) [#precondition-requirements-requires-state-params-query-body-] > \[!TIP] > **Faceted Requirements** > Middleware can declare compile-time preconditions across all 4 request facets: `params`, `query`, `body`, and `state`. > > ```ts > middleware().requires<{ > params?: { id: string }; > query?: { filter: string }; > body?: { token: string }; > state?: { user: User }; > }>(); > ``` For middlewares that require certain properties to exist in `state`, `req.params`, `req.query`, or `req.body` before running, use `.requires<{ ... }>()`: ```ts title="src/middleware/admin-guard.ts" import { middleware } from "@taserjs/router"; import { forbidden } from "@taserjs/router/reply"; type User = { id: string; role: "admin" | "superadmin" | "user"; }; export const requireAdmin = middleware() .requires<{ state: { user: User } }>() .handler(async ({ ctx, req, state }, next) => { // state.user is guaranteed by TypeScript to exist: if (state.user.role !== "admin" && state.user.role !== "superadmin") { return forbidden({ message: "Administrator privileges required" }); } return next({ isAdmin: true }); }); ``` ### Param-Guarded & Query-Guarded Middlewares [#param-guarded--query-guarded-middlewares] Middlewares can also enforce path parameters (validated against the route URL string e.g. `/users/:userId`) or query schemas provided by upstream layouts: ```ts title="src/middleware/load-user.ts" export const loadUser = middleware() .requires<{ params: { userId: string } }>() .handler(async ({ ctx, req }, next) => { const user = await db.users.findById(req.params.userId); return next({ user }); }); // Allowed: Route has ":userId" in its path t.get("/users/:userId/profile").use(loadUser)... // ❌ TypeScript Error: Route "/profile" has no :userId path parameter t.get("/profile").use(loadUser)... ``` ### Compile-Time Precondition Validation [#compile-time-precondition-validation] When `.use(...)` is added to a route or layout, TypeScript inspects the preceding middleware chain and route path. If the required facets are not satisfied, TypeScript produces a compile error: ```ts import { json } from "@taserjs/router/reply"; // ❌ TypeScript Error: Preconditions not satisfied t.get("/unprotected").use(requireAdmin); // Allowed: Upstream auth middleware provides { user: ... } t.get("/protected") .use(authMiddleware) .use(requireAdmin) .handler(() => json({ ok: true })); ``` *** ## Phased Route Builder Lifecycle [#phased-route-builder-lifecycle] Route definitions in Taser.js follow a strict phased lifecycle: 1. **Middleware Phase** (`.use(...)`) — Chained at the beginning of the route. 2. **Contract / Schema Phase** (`.query()`, `.params()`, `.body()`, `.returns()`) — Once schemas or return maps are declared, `.use()` is locked out to maintain deterministic execution order. 3. **Execution Phase** (`.handler(...)`) — The terminal route handler. ```ts export default t .get("/users/:id") // 1. Middlewares at the top: .use(cors()) .use(requireAdmin) // 2. Contracts and schemas: .query(z.object({ details: z.boolean().default(false) })) .returns({ 200: UserResponseSchema }) // 3. Handler: .handler(({ ctx, req, state }) => { return json({ id: req.params.id, isAdmin: state.isAdmin }); }); ``` *** ## Related Guides [#related-guides] # Extracting Helper Functions Canonical URL: https://taserjs.dev/docs/routing/refactoring-handlers Description: Extract reusable route handlers and split large API endpoints into small, testable modules with Taser.js compile-time Context type inference. In production applications, route handlers can easily grow into monolithic blocks of business logic, mixing input validation, database transactions, third-party payment processing, and notification dispatching. In traditional frameworks, breaking these monolithic handlers into smaller helper functions often forces developers to write verbose, manually duplicated TypeScript interfaces for the request context, params, and state. Taser.js makes decomposing handler logic **instant and 100% type-safe** by inferring handler argument types from the route builder. *** ## The Core Pattern [#the-core-pattern] To extract helper functions without repeating type definitions: 1. **Declare the Route Builder Variable First** (`GET`, `POST`, `PUT`, etc.). 2. **Extract `RouteContext`** using `Parameters[0]`. 3. **Write Small Helper Functions** that destructure `{ req, ctx, state }` from `RouteContext`. 4. **Call `.handler()`** on your route builder and orchestrate the helpers. ```ts title="src/routes/users/$id.get.ts" import { notFound, ok } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; // 1. Declare the verb builder const GET = t .get("/users/:id") .params(z.object({ id: z.string().uuid() })) .query(z.object({ includeOrders: z.coerce.boolean().default(false) })); // 2. Extract the exact handler argument type export type RouteContext = Parameters[0]; // [!code highlight] // 3. Extract focused helper functions async function fetchUser({ ctx, req }: RouteContext) { return ctx.db.user.findUnique({ where: { id: req.params.id }, // Typed as string (uuid) }); } async function fetchUserOrders({ ctx, req }: RouteContext) { if (!req.query.includeOrders) return []; // Typed as boolean return ctx.db.order.findMany({ where: { userId: req.params.id }, }); } // 4. Orchestrate inside the handler export default GET.handler(async (args) => { const user = await fetchUser(args); if (!user) { return notFound({ message: "User not found" }); } const orders = await fetchUserOrders(args); return ok({ user, orders }); }); ``` *** ## Real-World Example: Refactoring a Checkout Flow [#real-world-example-refactoring-a-checkout-flow] Consider an e-commerce checkout route that handles inventory verification, payment processing, database record creation, and email notifications. ### ❌ Before: Monolithic Handler [#-before-monolithic-handler] ```ts title="src/routes/checkout.post.ts" // A 100+ line handler that is difficult to test and maintain export default t .post("/checkout") .body( z.object({ itemId: z.string(), quantity: z.number().min(1), paymentMethodId: z.string(), }), ) .handler(async ({ ctx, req, state }) => { // 1. Check inventory inline... const item = await ctx.db.item.findUnique({ where: { id: req.body.itemId } }); if (!item || item.stock < req.body.quantity) { return badRequest({ message: "Out of stock" }); } // 2. Charge Stripe inline... const charge = await ctx.stripe.charges.create({ amount: item.price * req.body.quantity, currency: "usd", source: req.body.paymentMethodId, }); // 3. Save order to database inline... const order = await ctx.db.order.create({ data: { itemId: item.id, quantity: req.body.quantity, total: item.price * req.body.quantity, chargeId: charge.id, userId: state.user.id, }, }); // 4. Send email notification inline... await ctx.email.sendReceipt({ to: state.user.email, orderId: order.id }); return ok(order); }); ``` *** ### ✅ After: Clean Decomposition with `RouteContext` [#-after-clean-decomposition-with-routecontext] By extracting `CheckoutContext`, each step becomes a pure or focused async helper across modular service files: ```ts title="src/routes/checkout.post.ts" import { badRequest, ok } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; import { verifyStock } from "../services/inventory.js"; import { processPayment } from "../services/payment.js"; import { persistOrder } from "../services/order.js"; // 1. Declare Route Contract const POST = t .post("/checkout") .body( z.object({ itemId: z.string(), quantity: z.number().min(1), paymentMethodId: z.string(), }), ) .returns({ 200: z.object({ orderId: z.string(), status: z.literal("confirmed") }), 400: z.object({ error: z.string() }), }); // 2. Export Inferred Route Context for Helpers export type CheckoutContext = Parameters[0]; // 3. Clean Handler Orchestration export default POST.handler(async (args) => { const { ctx, state } = args; try { const item = await verifyStock(args); const charge = await processPayment(args, item); const order = await persistOrder(args, item, charge.id); // Background notification ctx.email.sendReceipt({ to: state.user.email, orderId: order.id }).catch(console.error); return ok({ orderId: order.id, status: "confirmed" }); } catch (err) { return badRequest({ error: (err as Error).message }); } }); ``` ```ts title="src/services/inventory.ts" import type { CheckoutContext } from "../routes/checkout.post.js"; export async function verifyStock({ ctx, req }: CheckoutContext) { const item = await ctx.db.item.findUnique({ where: { id: req.body.itemId }, }); if (!item || item.stock < req.body.quantity) { throw new Error("Item is out of stock"); } return item; } ``` ```ts title="src/services/payment.ts" import type { CheckoutContext } from "../routes/checkout.post.js"; export async function processPayment( { ctx, req }: CheckoutContext, item: { id: string; price: number }, ) { return ctx.stripe.charges.create({ amount: item.price * req.body.quantity, currency: "usd", source: req.body.paymentMethodId, }); } ``` ```ts title="src/services/order.ts" import type { CheckoutContext } from "../routes/checkout.post.js"; export async function persistOrder( { ctx, req, state }: CheckoutContext, item: { id: string; price: number }, chargeId: string, ) { return ctx.db.order.create({ data: { itemId: item.id, quantity: req.body.quantity, total: item.price * req.body.quantity, chargeId, userId: state.user.id, }, }); } ``` *** ## Granular Context Slicing with `Pick` [#granular-context-slicing-with-pick] When writing generic service functions or utilities that should not depend on the entire route context, you can use TypeScript's standard `Pick` utility: ```ts title="src/services/billing.ts" import type { CheckoutContext } from "../routes/checkout.post.js"; // Helper only requires ctx services and layout state type BillingContext = Pick; export async function processCustomerCharge( { ctx, state }: BillingContext, amountInCents: number, paymentMethodId: string, ) { ctx.logger.info(`Charging customer ${state.user.id}`); return ctx.stripe.charges.create({ amount: amountInCents, currency: "usd", source: paymentMethodId, customer: state.user.stripeCustomerId, }); } ``` This pattern enables **easy unit testing**: you can invoke `processCustomerCharge` with a lightweight mock object containing only `db`, `stripe`, and `state`. *** ## Moving Helpers to Separate Service Files [#moving-helpers-to-separate-service-files] For larger projects, you can organize helpers into dedicated service files: ```ts title="src/routes/users/$id.get.ts" import { notFound, ok } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; import { fetchUserProfile } from "../../services/user-service.js"; const GET = t .get("/users/:id") .params(z.object({ id: z.string().uuid() })); export type UserRouteContext = Parameters[0]; export default GET.handler(async (args) => { const profile = await fetchUserProfile(args); if (!profile) { return notFound({ message: "User not found" }); } return ok(profile); }); ``` ```ts title="src/services/user-service.ts" import type { UserRouteContext } from "../routes/users/$id.get.js"; export async function fetchUserProfile({ ctx, req }: UserRouteContext) { return ctx.db.user.findUnique({ where: { id: req.params.id }, include: { profile: true }, }); } ``` *** ## Why Infer from `GET`/`POST` Before `export default`? [#why-infer-from-getpost-before-export-default] Taser.js's `export default` architecture intentionally separates route contract declaration from handler execution, eliminating circular type hazards by design: 1. **Two-Phase Definition**: ```ts // Phase 1: Declare route contract (query, params, body, middlewares, returns) const GET = t.get("/users/:id").params(UserParamsSchema); // Phase 2: Inferred cleanly from the contract BEFORE execution logic runs export type RouteContext = Parameters[0]; async function fetchUser({ ctx, req }: RouteContext) { return ctx.db.findUser(req.params.id); } // Phase 3: Export the handler implementation export default GET.handler(async (args) => { const user = await fetchUser(args); return json(user); }); ``` 2. **No Post-Handler Type Hazard**: Because the handler is exported as `export default GET.handler(...)`, there is no monolithic post-execution variable holding both the contract and the handler implementation at the same time. `typeof default` is invalid TypeScript syntax, ensuring types are always derived from the upstream builder (`Parameters[0]`). ``` [Route Builder: const GET = t.get(...)] ──► Parameters[0] ──► [Helper Functions] │ │ ▼ ▼ [GET.handler(async ({ ctx, req, state }) => ...)] ◄────────────────────────────── [Orchestrate Helpers] │ ▼ [export default] ``` *** ## Summary of Benefits [#summary-of-benefits] | Feature | Monolithic Handlers | Taser.js facet-split handler helpers | | :-------------- | :------------------------------------ | :--------------------------------------------- | | **Readability** | 100+ line functions mixing concerns | 5 to 15 line orchestration handlers | | **Type Safety** | Implicit types inside single scope | Explicitly typed `ctx` across all functions | | **Testability** | Hard to isolate sub-operations | Every helper is an independently testable unit | | **Boilerplate** | Manually handwritten param interfaces | **Zero** handwritten interfaces, 100% inferred | # Handling Validation Errors Canonical URL: https://taserjs.dev/docs/validation/handling-errors Description: Understand built-in ValidationError (422) handling and customize envelopes with middleware try/catch — they bypass defineTaser().onError(). When incoming request parameters, query strings, headers, or JSON bodies fail validation, Taser.js throws a `ValidationError`. The runtime converts it to a **422** JSON response automatically. `ValidationError` (422) and `ResponseValidationError` (response contracts, 500) are handled by built-in runtime handlers. They never reach `defineTaser().onError()`, which is reserved for unhandled server crashes. Customize validation envelopes in middleware instead. *** ## Anatomy of a `ValidationError` [#anatomy-of-a-validationerror] The `ValidationError` class exposes the raw issues array returned by your schema validator: ```ts class ValidationError extends Error { readonly issues: readonly StandardSchemaV1.Issue[]; } ``` Each issue typically contains: * `path`: Array of property keys or array indices indicating where the error occurred (for example, `["body", "email"]` or `["query", "page"]`). * `message`: Human-readable error description (for example, `"Invalid email address"`). *** ## Default 422 Envelope [#default-422-envelope] Without customization, failed input validation returns: ```json { "errors": [ { "message": "Number must be greater than or equal to 1", "path": ["page"] }, { "message": "Invalid enum value", "path": ["category"] } ] } ``` *** ## Customizing Validation Envelopes in Middleware [#customizing-validation-envelopes-in-middleware] Catch `ValidationError` in a layout middleware `try/catch` before it reaches the built-in handler: ```ts title="src/routes/$.ts" import { ValidationError, middleware, t } from "@taserjs/router"; import { unprocessable } from "@taserjs/router/reply"; const formatValidationErrors = middleware(async (_ctx, next) => { try { return await next(); } catch (error) { if (error instanceof ValidationError) { const formattedErrors: Record = {}; for (const issue of error.issues) { const fieldName = issue.path?.map(String).join(".") || "root"; if (!formattedErrors[fieldName]) { formattedErrors[fieldName] = []; } formattedErrors[fieldName].push(issue.message); } return unprocessable({ status: 422, message: "The given data was invalid.", errors: formattedErrors, }); } throw error; } }); export default t.layout("/*").use(formatValidationErrors); ``` *** ## Example Custom Error Response [#example-custom-error-response] Given a request with invalid query parameters: ```bash curl "http://localhost:3000/products?page=-5&category=unknown" ``` With the middleware above, the client receives: ```json { "status": 422, "message": "The given data was invalid.", "errors": { "query.page": ["Number must be greater than or equal to 1"], "query.category": ["Invalid enum value. Expected 'electronics' | 'clothing' | 'books'"] } } ``` *** ## Auto-Generated 422 Response Schemas [#auto-generated-422-response-schemas] When you attach input schemas (`query`, `params`, `body`) to a route, Taser.js automatically documents a `422` error contract for that endpoint. The typed client SDK also understands that the route can return `422` validation issues. *** ## Related Guides [#related-guides] # Middleware Validation Canonical URL: https://taserjs.dev/docs/validation/middleware-validation Description: 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 [#state-inference--injection] Layout middleware can validate incoming requests and inject typed state down the pipeline. Any object passed to `next({ ... })` is merged into the **`state`** facet and inferred across all child handlers: ```ts title="src/routes/orgs.ts" 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, req }, next) => { const orgSlug = req.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 } on state for all downstream routes: return next({ organization: org }); }); ``` *** ## Validating Query, Params, and Body in Middleware [#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: ```ts title="src/routes/api/v2.ts" 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, req }, next) => { // req.query.apiKey is validated const keyRecord = await ctx.db.findApiKey(req.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) [#validation-only-middleware-optional-handler] When a middleware is used purely for schema validation, parameter coercion, or request parsing, calling `.handler()` is completely optional: ```ts title="src/middleware/pagination.ts" 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 `req.query.page` and `req.query.limit` without any boilerplate: ```ts title="src/routes/posts.get.ts" 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, req }) => { // req.query.page and req.query.limit are automatically typed as numbers const posts = await ctx.db.getPosts({ page: req.query.page, limit: req.query.limit }); return json(posts); }); ``` *** ## Multi-Step Middleware Pipelines [#multi-step-middleware-pipelines] You can chain multiple `.use()` calls on a layout middleware. Each step enriches **`state`** and subsequent middleware steps immediately receive previously injected state: ```ts title="src/routes/billing.ts" import { unauthorized } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t .layout("/billing") // Step 1: Validate session & inject userId .use(async ({ req }, next) => { const token = req.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, state }, next) => { // state.userId is 100% typed from Step 1! const sub = await ctx.db.getSubscription(state.userId); return next({ hasActiveSubscription: Boolean(sub?.active), }); }); ``` *** ## Preconditions (`.requires<{ state?, params?, query?, body? }>()`) [#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()`: ```ts title="src/middleware/require-subscription.ts" import { middleware } from "@taserjs/router"; import { forbidden } from "@taserjs/router/reply"; type SubscriptionPrecondition = { hasActiveSubscription: boolean; }; export const requireActivePlan = middleware() .requires<{ state: SubscriptionPrecondition }>() .handler(({ state }, next) => { // TypeScript ensures state.hasActiveSubscription exists: if (!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 [#next-steps] # Standard Schema Validation Canonical URL: https://taserjs.dev/docs/validation/standard-schema Description: Validate query parameters, path params, request bodies, and headers using Zod, ArkType, Valibot, or any Standard Schema library. Taser.js natively adheres to the **Standard Schema** specification (`@standard-schema/spec`). This means you have zero vendor lock-in. You can use **Zod**, **ArkType**, **Valibot**, or any other compliant validation library without adapters or wrappers. *** ## Path Parameters (`.params`) [#path-parameters-params] ### Default Behavior [#default-behavior] All path parameters defined in your route filenames (e.g. `src/routes/users/$id.get.ts` or `src/routes/orgs/$orgId/repos/$repoId.get.ts`) are **automatically inferred as `string` types** on `req.params` by default—no configuration required: ```ts title="src/routes/users/$id.get.ts" import { json } from "@taserjs/router/reply"; import { t } from "@taserjs/router"; export default t.get("/users/:id").handler(async ({ ctx, req }) => { // req.params.id is inferred as string by default: const user = await ctx.db.getUser(req.params.id); return json(user); }); ``` ### Validating & Coercing Path Parameters [#validating--coercing-path-parameters] When you need strict validation (such as UUID formats, integer IDs, or enum values), chain `.params()` with a schema: ```ts title="src/routes/organizations/$orgId/members/$memberId.get.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .get("/organizations/:orgId/members/:memberId") .params( z.object({ orgId: z.string().uuid(), memberId: z.coerce.number().int(), }), ) .handler(async ({ ctx, req }) => { // req.params.orgId is a validated UUID string // req.params.memberId is a coerced number const member = await ctx.db.getMember(req.params.orgId, req.params.memberId); return json(member); }); ``` *** ## Query Parameters (`.query`) [#query-parameters-query] HTTP query parameters always arrive from the URL as strings. Use your schema library to coerce numbers, booleans, and dates: ```ts title="src/routes/products.get.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .get("/products") .query( z.object({ search: z.string().optional(), category: z.enum(["electronics", "clothing", "books"]).optional(), page: z.coerce.number().int().min(1).default(1), limit: z.coerce.number().int().min(1).max(100).default(20), inStockOnly: z.coerce.boolean().default(false), }), ) .handler(async ({ ctx, req }) => { // req.query.page is number // req.query.inStockOnly is boolean const results = await ctx.db.searchProducts(req.query); return json(results); }); ``` ```ts title="src/routes/products.get.ts" import { json } from "@taserjs/router/reply"; import { type } from "arktype"; import { t } from "@taserjs/router"; const QuerySchema = type({ "search?": "string", "category?": "'electronics' | 'clothing' | 'books'", "page?": "string.integer.parse >= 1 = 1", "limit?": "string.integer.parse <= 100 = 20", }); export default t .get("/products") .query(QuerySchema) .handler(async ({ ctx, req }) => { const results = await ctx.db.searchProducts(req.query); return json(results); }); ``` ```ts title="src/routes/products.get.ts" import { json } from "@taserjs/router/reply"; import * as v from "valibot"; import { t } from "@taserjs/router"; const QuerySchema = v.object({ search: v.optional(v.string()), page: v.fallback(v.pipe(v.string(), v.transform(Number), v.integer()), 1), limit: v.fallback(v.pipe(v.string(), v.transform(Number), v.integer()), 20), }); export default t .get("/products") .query(QuerySchema) .handler(async ({ ctx, req }) => { const results = await ctx.db.searchProducts(req.query); return json(results); }); ``` *** ## Request Bodies & Body Modes (`.body`) [#request-bodies--body-modes-body] Taser.js gives you fine-grained control over request body parsing and performance: * **Zero Overhead**: If a route does not call `.body()`, incoming body parsing is **completely skipped**. * **Default JSON Parsing**: Calling `.body(schema)` parses the body as JSON by default. * **Explicit Body Modes**: To parse file uploads, URL-encoded forms, raw text, or binary buffers, provide the mode as the first argument (`.body(mode, schema)`): | Body Mode | Method Signature | Use Case | Content Types | | :------------------------------- | :----------------------------------------- | :-------------------------------- | :---------------------------------- | | **`json`** *(default)* | `.body(schema)` or `.body("json", schema)` | REST JSON payloads | `application/json` | | **`form`** | `.body("form", schema)` | File uploads and form submissions | `multipart/form-data` | | **`urlencoded`** | `.body("urlencoded", schema)` | URL-encoded form data | `application/x-www-form-urlencoded` | ### JSON Body Example [#json-body-example] ```ts title="src/routes/posts.post.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const CreatePostSchema = z.object({ title: z.string().min(5).max(200), slug: z.string().regex(/^[a-z0-9-]+$/), content: z.string().min(20), published: z.boolean().default(false), tags: z.array(z.string()).max(5).default([]), }); export default t .post("/posts") .body(CreatePostSchema) .handler(async ({ ctx, req }) => { // req.body is inferred directly from CreatePostSchema: const newPost = await ctx.db.posts.create({ data: req.body, }); return json({ id: newPost.id, slug: newPost.slug }, { status: 201 }); }); ``` *** ## Validating Multipart Form Data & File Uploads [#validating-multipart-form-data--file-uploads] When handling file uploads or form data, pass `"form"` as the first argument to `.body()`. Taser.js automatically parses form fields and converts binary file uploads into standard JavaScript `File` objects. You can validate file presence, file size, and permitted MIME types using your schema validator: ```ts title="src/routes/users/$id/avatar.post.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB const ACCEPTED_IMAGE_TYPES = ["image/jpeg", "image/png", "image/webp"]; const AvatarUploadSchema = z.object({ caption: z.string().optional(), avatar: z .file("Avatar must be a valid file") .refine((file) => file.size <= MAX_FILE_SIZE, "Max image size is 5MB") .refine( (file) => ACCEPTED_IMAGE_TYPES.includes(file.type), "Only .jpg, .png, and .webp formats are supported" ), }); export default t .post("/users/:id/avatar") .params(z.object({ id: z.string() })) .body("form", AvatarUploadSchema) // [!code highlight] .handler(async ({ ctx, req }) => { const { avatar, caption } = req.body; // Access Web Standard File methods directly: const fileBuffer = await avatar.arrayBuffer(); const fileName = avatar.name; const fileSize = avatar.size; await ctx.db.saveUserAvatar(req.params.id, fileBuffer, fileName); return json({ success: true, fileName, bytes: fileSize, }); }); ``` ```ts title="src/routes/users/$id/avatar.post.ts" import { json } from "@taserjs/router/reply"; import { type } from "arktype"; import { t } from "@taserjs/router"; const AvatarUploadSchema = type({ "caption?": "string", avatar: type("instanceof.File").narrow((file, ctx) => { if (file.size > 5 * 1024 * 1024) { return ctx.mustBe("smaller than 5MB"); } return true; }), }); export default t .post("/users/:id/avatar") .body("form", AvatarUploadSchema) // [!code highlight] .handler(async ({ ctx, req }) => { const { avatar } = req.body; const fileBuffer = await avatar.arrayBuffer(); return json({ success: true, fileName: avatar.name }); }); ``` ```ts title="src/routes/users/$id/avatar.post.ts" import { json } from "@taserjs/router/reply"; import * as v from "valibot"; import { t } from "@taserjs/router"; const AvatarUploadSchema = v.object({ caption: v.optional(v.string()), avatar: v.pipe( v.instance(File, "Avatar must be a file"), v.check((file) => file.size <= 5 * 1024 * 1024, "Max file size is 5MB") ), }); export default t .post("/users/:id/avatar") .body("form", AvatarUploadSchema) // [!code highlight] .handler(async ({ ctx, req }) => { const { avatar } = req.body; const buffer = await avatar.arrayBuffer(); return json({ success: true, fileName: avatar.name }); }); ``` *** ## Multiple File Uploads [#multiple-file-uploads] When clients upload multiple files under the same form field name, validate them with `.body("form", schema)` as an array of `File` objects: ```ts title="src/routes/documents/upload.post.ts" import { json } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; const MultiDocSchema = z.object({ folderId: z.string(), // Handles single or multiple files seamlessly: files: z .union([z.file(), z.array(z.file())]) .transform((val) => (Array.isArray(val) ? val : [val])), }); export default t .post("/documents/upload") .body("form", MultiDocSchema) // [!code highlight] .handler(async ({ ctx, req }) => { // req.body.files is typed as File[]: for (const file of req.body.files) { const content = await file.text(); console.log(`Uploaded file: ${file.name} (${file.size} bytes)`); } return json({ uploadedCount: req.body.files.length }); }); ``` *** ## Client-Side Uploads with `formBody()` [#client-side-uploads-with-formbody] When consuming your multipart endpoints using `@taserjs/client`, wrap your payload with `formBody()`: ```ts import { formBody } from "@taserjs/client"; import { api } from "./lib/api"; const selectedFile = fileInput.files[0]; // The client sends multipart/form-data with automatically managed boundary headers: const response = await api.users._id.avatar.$post({ param: { id: "user_123" }, body: formBody({ caption: "My new profile picture", avatar: selectedFile, }), }); ``` *** ## Schema Transformations and Inferred Types [#schema-transformations-and-inferred-types] Taser.js respects schema transformations. If your schema takes a string and transforms it into a `Date` or trimmed string, `req.body` receives the transformed output type: ```ts title="src/routes/events.post.ts" import { ok } from "@taserjs/router/reply"; import { z } from "zod"; import { t } from "@taserjs/router"; export default t .post("/events") .body( z.object({ title: z.string().trim(), scheduledAt: z .string() .datetime() .transform((str) => new Date(str)), }), ) .handler(async ({ ctx, req }) => { // req.body.scheduledAt is typed as Date, not string! console.log(req.body.scheduledAt.getTime()); return ok(); }); ``` *** ## Related Guides [#related-guides]