Context API
The ctx object provides unified access to request and response in every handler
The ctx object is your primary interface in NextRush. It provides unified access to request data and response methods in a single, coherent object.
The Problem
Traditional Node.js frameworks split request and response:
app.get('/users/:id', (req, res) => {
const id = req.params.id; // From req
const query = req.query; // From req
res.json({ id, query }); // To res
});When middleware needs to share data, you mutate req:
app.use((req, res, next) => {
req.user = { id: 123 }; // Mutate req
next();
});This works, but it's unclear what properties exist on req at any point. TypeScript can't help you, and bugs hide in the ambiguity.
How NextRush Approaches This
NextRush combines everything into a single Context object:
app.get('/users/:id', (ctx) => {
const id = ctx.params.id; // Request data
const query = ctx.query; // Request data
ctx.json({ id, query }); // Response method
});For sharing data between middleware, use the typed state bag:
// Auth middleware
app.use(async (ctx) => {
ctx.state.user = { id: 123 };
await ctx.next();
});
// Handler
app.get('/profile', (ctx) => {
ctx.json({ user: ctx.state.user });
});Mental Model
Think of Context as a request envelope that travels through your middleware pipeline:
┌─────────────────────────────────────────────┐
│ Context (ctx) │
├─────────────────────────────────────────────┤
│ ┌─────────────────────────────────────┐ │
│ │ INPUT (Request) │ │
│ │ • method, path, url │ │
│ │ • params, query, headers │ │
│ │ • body (after parser) │ │
│ │ • ip │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ OUTPUT (Response) │ │
│ │ • status │ │
│ │ • json(), send(), html() │ │
│ │ • set(), redirect() │ │
│ └─────────────────────────────────────┘ │
│ ┌─────────────────────────────────────┐ │
│ │ SHARED (State) │ │
│ │ • state.user, state.requestId... │ │
│ └─────────────────────────────────────┘ │
└─────────────────────────────────────────────┘Request Properties
Request Properties Summary
| Property | Type | Description |
|---|---|---|
method | HttpMethod | HTTP request method (GET, POST, etc.) |
path | string | Request path without query string |
url | string | Full URL including query string |
params | Record<string, string> | Route parameters from the router |
query | Record<string, string | string[] | undefined> | Parsed query string parameters |
headers | Record<string, string | string[] | undefined> | Request headers (lowercased keys in Node.js) |
body | unknown | Request body (requires body parser middleware) |
ip | string | Client IP address (respects X-Forwarded-For when proxy: true) |
Detailed request property examples
ctx.method
ctx.method; // 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH' | 'HEAD' | 'OPTIONS' | ...ctx.path
// Request: GET /users/123?include=posts
ctx.path; // '/users/123'ctx.url
// Request: GET /users/123?include=posts
ctx.url; // '/users/123?include=posts'ctx.params
// Route: /users/:id
// Request: GET /users/123
ctx.params; // { id: '123' }
ctx.params.id; // '123'ctx.query
// Request: GET /users?page=2&limit=10
ctx.query; // { page: '2', limit: '10' }
ctx.query.page; // '2'ctx.headers
Use ctx.get() for case-insensitive lookup across all runtimes.
ctx.headers['content-type']; // 'application/json'
ctx.headers['authorization']; // 'Bearer xxx'ctx.body
import { json } from '@nextrush/body-parser';
app.use(json());
app.post('/users', (ctx) => {
const { name, email } = ctx.body as { name: string; email: string };
});ctx.ip
ctx.ip; // '192.168.1.1'ctx.get(header)
ctx.get('Content-Type'); // 'application/json'
ctx.get('Authorization'); // 'Bearer xxx'
ctx.get('x-request-id'); // 'abc-123'The body property deserves special attention — it is not available by default.
Body Requires Parser
ctx.body is undefined until a body parser middleware runs. Always add a body parser before
accessing ctx.body.
Response Methods
Response Methods Summary
| Property | Type | Description |
|---|---|---|
json(data) | (data: unknown) => void | Send JSON response with Content-Type: application/json |
send(data) | (data: unknown) => void | Send string, Buffer, stream, or object response |
html(content) | (content: string) => void | Send HTML response with Content-Type: text/html |
redirect(url, status?) | (url: string, status?: number) => void | Redirect to URL (default: 302) |
set(field, value) | (field: string, value: string | number | string[]) => void | Set response header |
status | number | Get or set response status code (default: 200) |
Detailed response method examples
ctx.json(data)
ctx.json({ users: [] });
ctx.json({ error: 'Not found' });Sets Content-Type: application/json; charset=utf-8, serializes with JSON.stringify().
ctx.send(data)
Handles strings, Buffer, Uint8Array, ArrayBuffer, Node.js streams, Web ReadableStream, objects (delegates to json()), and null.
ctx.send('Hello World'); // text/plain
ctx.send(Buffer.from([0x48, 0x69])); // application/octet-stream
ctx.send(readableStream); // pipes streamctx.html(content)
ctx.html('<h1>Hello World</h1>');ctx.redirect(url, status?)
ctx.redirect('/login'); // 302 Found
ctx.redirect('/new-page', 301); // 301 Moved Permanentlyctx.set(field, value)
ctx.set('X-Request-Id', 'abc-123');
ctx.set('Cache-Control', 'no-store');
ctx.set('Set-Cookie', ['a=1; Path=/', 'b=2; Path=/']);ctx.status
ctx.status = 201; // Set status
console.log(ctx.status); // Get statusError Helpers
ctx.throw(status, message?)
Throw an HTTP error that stops the pipeline.
ctx.throw(404); // Not Found
ctx.throw(404, 'User not found'); // With custom message
ctx.throw(401, 'Invalid token');ctx.assert(condition, status, message?)
Assert a condition or throw.
const user = await getUser(id);
ctx.assert(user, 404, 'User not found');
ctx.assert(user.isAdmin, 403, 'Forbidden');
// Equivalent to:
if (!user) {
ctx.throw(404, 'User not found');
}Middleware Flow
ctx.next()
Call the next middleware in the chain.
app.use(async (ctx) => {
console.log('Before');
await ctx.next(); // Call next middleware
console.log('After');
});Two Syntaxes
Both syntaxes work identically:
// Modern: ctx.next()
app.use(async (ctx) => {
await ctx.next();
});
// Koa-style: next parameter
app.use(async (ctx, next) => {
await next();
});ctx.state
A mutable object for sharing data between middleware.
// Auth middleware
app.use(async (ctx) => {
const token = ctx.get('Authorization');
if (token) {
ctx.state.user = await verifyToken(token);
}
await ctx.next();
});
// Handler
app.get('/profile', (ctx) => {
ctx.json({ user: ctx.state.user });
});Platform Access
ctx.raw
Access platform-specific request/response objects.
// Node.js
ctx.raw.req; // http.IncomingMessage
ctx.raw.res; // http.ServerResponse
// Bun/Deno/Edge
ctx.raw.req; // Web Request objectEscape Hatch
Use ctx.raw only when you need platform-specific features. Prefer NextRush's cross-platform API
for compatibility.
ctx.runtime
Detect the current runtime.
ctx.runtime;
// 'node' | 'bun' | 'deno' | 'deno-deploy'
// | 'cloudflare-workers' | 'vercel-edge' | 'edge' | 'unknown'
if (ctx.runtime === 'bun') {
// Bun-specific optimization
}ctx.responded
Whether a response has already been sent. Useful in middleware that runs after downstream handlers.
app.use(async (ctx) => {
await ctx.next();
if (!ctx.responded) {
ctx.status = 404;
ctx.json({ error: 'Not found' });
}
});ctx.bodySource
Low-level body source for reading raw request data. Most applications use ctx.body (set by body-parser middleware) instead. Useful when building custom body parsers.
// In a custom body parser
const text = await ctx.bodySource.text();
const data = JSON.parse(text);What Happens Automatically
Context handles several things without configuration:
- Status defaults to 200 — set
ctx.statusbefore calling a response method to override - Content-Type detection —
json()setsapplication/json,html()setstext/html,send()infers from data type - Content-Length — set automatically for string, buffer, and JSON responses
- HEAD request handling — response methods suppress the body for
HEADrequests,204, and304status codes - Query parsing —
ctx.queryis parsed from the URL automatically - Body is undefined by default — requires body-parser middleware to populate
Common Patterns
Request Logging
app.use(async (ctx) => {
const start = Date.now();
console.log(`→ ${ctx.method} ${ctx.path}`);
await ctx.next();
const ms = Date.now() - start;
console.log(`← ${ctx.status} (${ms}ms)`);
});Request ID Propagation
app.use(async (ctx) => {
const requestId = ctx.get('X-Request-Id') || crypto.randomUUID();
ctx.state.requestId = requestId;
ctx.set('X-Request-Id', requestId);
await ctx.next();
});Authentication
app.use(async (ctx) => {
const token = ctx.get('Authorization')?.replace('Bearer ', '');
if (token) {
try {
ctx.state.user = await verifyJWT(token);
} catch {
// Invalid token - continue without user
}
}
await ctx.next();
});Common Mistakes
Forgetting await on next()
// ❌ Wrong: Response may be sent before downstream completes
app.use(async (ctx) => {
ctx.next(); // Missing await!
ctx.set('X-After', 'value');
});
// ✅ Correct
app.use(async (ctx) => {
await ctx.next();
ctx.set('X-After', 'value');
});Multiple Responses
// ❌ Wrong: Two responses
app.use((ctx) => {
ctx.json({ step: 1 });
ctx.json({ step: 2 }); // Error or ignored
});
// ✅ Correct: One response
app.use((ctx) => {
ctx.json({ steps: [1, 2] });
});Accessing Body Without Parser
// ❌ Wrong: body is undefined
app.post('/users', (ctx) => {
console.log(ctx.body); // undefined!
});
// ✅ Correct: Add body parser first
import { json } from '@nextrush/body-parser';
app.use(json());
app.post('/users', (ctx) => {
console.log(ctx.body); // { name: 'Alice', ... }
});TypeScript Types
Import context types for type annotations:
import type { Context, ContextState } from '@nextrush/types';
const handler = (ctx: Context) => {
ctx.json({ ok: true });
};
// Extend state type
interface AppState extends ContextState {
user?: { id: string; email: string };
requestId: string;
}Quick Reference
Request Properties (read-only)
| Property | Type | Description |
|---|---|---|
method | HttpMethod | HTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE, CONNECT) |
url | string | Full URL including query string |
path | string | Request path without query string |
query | Record<string, string | string[] | undefined> | Parsed query string parameters |
headers | Record<string, string | string[] | undefined> | Request headers |
ip | string | Client IP address |
runtime | Runtime | Current JavaScript runtime |
raw | RawHttp | Platform-specific request/response objects |
bodySource | BodySource | Low-level body source for custom parsers |
responded | boolean | Whether a response has been sent |
Mutable Properties
| Property | Type | Description |
|---|---|---|
body | unknown= undefined | Parsed request body (set by body-parser middleware) |
params | Record<string, string> | Route parameters (set by router) |
status | number= 200 | Response status code |
state | Record<string | symbol, unknown> | Request-scoped state for middleware data sharing |
Response Methods
| Property | Type | Description |
|---|---|---|
json(data) | (data: unknown) => void | Send JSON response |
send(data) | (data: ResponseBody) => void | Send string, buffer, stream, or object response |
html(content) | (content: string) => void | Send HTML response |
redirect(url, status?) | (url: string, status?: number) => void | Redirect (default 302) |
set(field, value) | (field: string, value: string | number | string[]) => void | Set response header |
get(field) | (field: string) => string | undefined | Get request header (case-insensitive) |
throw(status, message?) | (status: number, message?: string) => never | Throw HTTP error |
assert(condition, status, message?) | (condition: unknown, status: number, message?: string) => asserts condition | Assert or throw |
next() | () => Promise<void> | Call next middleware |