Context Propagation
Context propagation allows type-safe, request-scoped state sharing across middleware and route handlers. Built on TypeScript, it enables -
- Fine-grained control over request data
- Modular architecture
- Developer productivity with strict typing and tooling support
1. Conceptual Overview
- Type-safe context shape
- Scoped to individual requests
- Mutable across middleware
- Extended through type composition
2. Defining a Typed Context
Create a strongly-typed interface to define your request context structure:
interface CustomContext {
user?: { id: number; email: string }; // Optional user data
requestId: string; // Required unique identifier per request
}3. Typed Router / App Instantiation
Pass the context type to Router or TezX for strict type enforcement:
import { TezX } from "tezx";
const app = new TezX<CustomContext>();4. Injecting Data via Middleware
Use middleware to enrich the context during the request lifecycle:
app.use(async (ctx, next) => {
ctx.user = await authenticate(ctx.req);
return next();
});
app.use((ctx, next) => {
ctx.requestId = crypto.randomUUID();
return next();
});5. Typed Context Access in Handlers
Access your context properties safely with full TypeScript support:
app.get("/me", (ctx) => {
return ctx.json({
requestId: ctx.requestId,
email: ctx.user?.email ?? "Anonymous",
});
});6. Context Composition
Scale context by composing smaller domain-specific types:
interface AuthContext {
user: { id: number; email: string };
session: { token: string; expires: Date };
}
interface MetricsContext {
logger: Logger;
startTime: number;
}
type AppContext = AuthContext & MetricsContext;
const app = new TezX<AppContext>();7. Validation Middleware
Ensure critical fields are present at runtime:
app.use((ctx, next) => {
if (!ctx.requestId) throw new Error("Missing request ID");
return next();
});8. Type Safety Guards
TypeScript prevents invalid usage:
// ❌ Property 'newProp' does not exist
ctx.newProp = "value";
// ❌ Type mismatch
ctx.requestId = 123; // should be a string9. Optional Properties Handling
Use safe access patterns for optional context data:
app.get("/dashboard", (ctx) => {
if (!ctx.user) return ctx.status(401).json({ error: "Unauthorized" });
return ctx.json({ email: ctx.user.email });
});10. Best Practices
-
Order Matters Set required context early and validate immediately:
app.use((ctx, next) => { ctx.requestId = crypto.randomUUID(); return next(); }); app.use((ctx, next) => { if (!ctx.requestId) throw new Error("Missing ID"); return next(); }); -
Immutability Prefer immutable patterns when modifying nested objects:
ctx.user = { ...ctx.user, email: "updated@example.com" }; -
Testing with Mock Context Test logic in isolation with mock context:
const mockCtx: CustomContext = { requestId: "req-test-001", user: { id: 1, email: "test@example.com" }, }; const response = await handler(mockCtx);
12. Why Context Propagation?
| Benefit | Description |
|---|---|
| 🔐 Type Safety | Compile-time validation prevents access errors and invalid mutations |
| 🧩 Modularity | Enables context slicing and extension across layers |
| ⚙️ Maintainability | Clear structure for evolving app concerns like auth, logging, sessions |
| 🚀 Scalability | Composable and predictable patterns for large applications |
Context
The Context class is the central abstraction for handling requests and responses in TezX. It wraps a request, manages headers, status codes, body, and provides helper methods for sending different types of responses including text, HTML, JSON, files, and redirects.
Middleware Merging
This outlines how two router trees with nested middlewares are intelligently merged to preserve execution order and structure.