Routing System

Context and State

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

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:

src/taser.ts
import { createTaserApp, type InferAppContext } from "@taserjs/router";
import { context } from "./context.js";

export default createTaserApp().context(context);

export type AppContext = InferAppContext<typeof context>;

Boot Context vs Request Context

FeatureBoot Context (boot)Request Context (request)
ExecutionRuns once when the application boots upRuns once for each incoming request
Async SupportFully async (async () => ({ ... }))Sync or async ((req: Request) => ({ ... }))
Best Used ForDatabase pools, Redis connections, SDK clients, configurationRequest IDs, timing markers, trace headers, tenancy IDs
PerformanceZero per-request overheadLightweight object allocation per request

Accessing Context in Handlers

Every route handler and middleware automatically receives the combined properties of boot and request context directly on the ctx object:

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) => {
  // Available from boot context:
  const user = await ctx.db.user.findUnique({
    where: { id: ctx.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

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():

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 = typeof GET.$Infer.Context;

// 3. Type your helper functions with RouteContext
async function getUserProfile(ctx: RouteContext) {
  return ctx.db.user.findUnique({
    where: { id: ctx.params.id },
  });
}

// 4. Call helper inside .handler()
export default GET.handler(async (ctx) => {
  const user = await getUserProfile(ctx);
  if (!user) {
    return notFound({ message: "User not found" });
  }
  return json(user);
});

Accessing the Standard Request in createContext

The request hook receives the standard Web Request object for each incoming dispatch:

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

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