Getting Started

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


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

Stage 01RUNTIME

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
Generated Entry: src/.taserjs/routes.gen.ts exports app and AppManifest
Host Integration: Standalone Vite/Nitro, Next.js plugin, or host pass-through (Express, Fastify, Hono)
Dispatch: await app.fetch(request) → Web Standard Response
src/.taserjs/routes.gen.tsTypeScript
// createTaserApp(routeManifest, taser) at build time
export const app = /* Hono TaserApp */

// Per request (any host):
const response = await app.fetch(request)
1 of 7

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.

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)

Route handlers and middleware use facet-split arguments instead of a single flattened context object:

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

Prop

Type

Cookies are not available until you mount cookie() on a layout. Handlers under that layout then receive a Cookie Jar Instance as { cookies }:

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! }));
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

Understanding when to place properties in Application Context (createContext) versus middleware state is essential for maintaining clean architecture:

Architectural DimensionApplication Context (createContext)Middleware state (state)
Locationsrc/context.ts (attached in src/taser.ts)Folder layouts (admin.ts, dashboard.ts, _auth.ts, …)
LifecycleBoot (singletons) & Per-Request hooksPer-request during middleware pipeline traversal
ScopeGlobal: Accessible across every route and middleware in the appScoped: Accessible only to child routes located under that directory
Access Syntaxctx.db, ctx.logger, ctx.requestIdstate.user, state.session, state.jwtPayload in handlers
Declaration TypedefineTaser().context(context)Inferred from return next({ ... }) in layouts
Primary Use CasesDatabase pools, Redis clients, queue producers, correlation IDsAuthenticated users, RBAC permissions, tenant metadata

Code Comparison

1. Global Infrastructure in src/context.ts

Use createContext to inject server singletons and universal request properties:

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

Use next({ ... }) in layout middleware to compute and pass typed state downstream:

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,
  });
});

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.