Core Concepts
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
1. Filesystem Routing
Deterministic file conventions define URLs and HTTP verbs automatically without manual route registries.
2. Dual-Layer Context
Separate long-lived boot singletons (databases, caches) from request-scoped metadata (IDs, timing) with createContext.
3. Cascading Pipelines
Middleware state returned in folder layouts flows down the directory tree on the state facet with full inference.
4. Bidirectional Safety
Standard Schema validates incoming params, query, and bodies, while .returns() enforces outgoing response shapes.
The Request Lifecycle
Every HTTP request handled by Taser.js flows through a deterministic, type-safe pipeline:
Taser.js Request Execution Pipeline
Click any stage to inspect lifecycle execution, data flow, and type inference
TaserApp Dispatch
@taserjs/plugin and @taserjs/cli emit a static route manifest into routes.gen.ts. createTaserApp() registers composed handlers on a Hono TaserApp once; each request is served via the Web fetch API with no filesystem routing at runtime.
Execution Highlights
// createTaserApp(routeManifest, taser) at build time
export const app = /* Hono TaserApp */
// Per request (any host):
const response = await app.fetch(request)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
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
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
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
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
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
src/taser.ts exports an uninstantiated TaserDefinition from defineTaser() — configuration only (context, basePath, notFound, onError, response). It does not create the Hono app.
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 (alsodefault)- Ambient
RouterRegisteraugmentations —RoutePath, layout hierarchy, andAppContext - Manifest types — including the composite
AppManifestused 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)
Route handlers and middleware use facet-split arguments instead of a single flattened context object:
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
Prop
Type
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 }:
import { cookie } from "@taserjs/router/middleware/cookie";
import { t } from "@taserjs/router";
export default t.layout("/*").use(cookie({ secret: process.env.COOKIE_SECRET! }));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
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
1. Global Infrastructure in src/context.ts
Use createContext to inject server singletons and universal request properties:
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
Use next({ ... }) in layout middleware to compute and pass typed state downstream:
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,
});
});No Global Interface Merging
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
Manual Installation
Step-by-step guide to installing and configuring Taser.js manually with Vite, Nitro deployment presets, Next.js, and host pass-through dispatching.
Migration Guide
Incrementally migrate existing Express or Fastify APIs to Taser.js with zero downtime using the host pass-through architecture.