NextRush
Concepts

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:

Express Pattern
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:

Express Middleware
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:

NextRush Pattern
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:

State Sharing
// 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

PropertyTypeDescription
methodHttpMethodHTTP request method (GET, POST, etc.)
pathstringRequest path without query string
urlstringFull URL including query string
paramsRecord<string, string>Route parameters from the router
queryRecord<string, string | string[] | undefined>Parsed query string parameters
headersRecord<string, string | string[] | undefined>Request headers (lowercased keys in Node.js)
bodyunknownRequest body (requires body parser middleware)
ipstringClient 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

PropertyTypeDescription
json(data)(data: unknown) => voidSend JSON response with Content-Type: application/json
send(data)(data: unknown) => voidSend string, Buffer, stream, or object response
html(content)(content: string) => voidSend HTML response with Content-Type: text/html
redirect(url, status?)(url: string, status?: number) => voidRedirect to URL (default: 302)
set(field, value)(field: string, value: string | number | string[]) => voidSet response header
statusnumberGet 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 stream

ctx.html(content)

ctx.html('<h1>Hello World</h1>');

ctx.redirect(url, status?)

ctx.redirect('/login'); // 302 Found
ctx.redirect('/new-page', 301); // 301 Moved Permanently

ctx.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 status

Error 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 object

Escape 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.status before calling a response method to override
  • Content-Type detectionjson() sets application/json, html() sets text/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 HEAD requests, 204, and 304 status codes
  • Query parsingctx.query is 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)

PropertyTypeDescription
methodHttpMethodHTTP method (GET, POST, PUT, DELETE, PATCH, HEAD, OPTIONS, TRACE, CONNECT)
urlstringFull URL including query string
pathstringRequest path without query string
queryRecord<string, string | string[] | undefined>Parsed query string parameters
headersRecord<string, string | string[] | undefined>Request headers
ipstringClient IP address
runtimeRuntimeCurrent JavaScript runtime
rawRawHttpPlatform-specific request/response objects
bodySourceBodySourceLow-level body source for custom parsers
respondedbooleanWhether a response has been sent

Mutable Properties

PropertyTypeDescription
bodyunknown= undefinedParsed request body (set by body-parser middleware)
paramsRecord<string, string>Route parameters (set by router)
statusnumber= 200Response status code
stateRecord<string | symbol, unknown>Request-scoped state for middleware data sharing

Response Methods

PropertyTypeDescription
json(data)(data: unknown) => voidSend JSON response
send(data)(data: ResponseBody) => voidSend string, buffer, stream, or object response
html(content)(content: string) => voidSend HTML response
redirect(url, status?)(url: string, status?: number) => voidRedirect (default 302)
set(field, value)(field: string, value: string | number | string[]) => voidSet response header
get(field)(field: string) => string | undefinedGet request header (case-insensitive)
throw(status, message?)(status: number, message?: string) => neverThrow HTTP error
assert(condition, status, message?)(condition: unknown, status: number, message?: string) => asserts conditionAssert or throw
next()() => Promise<void>Call next middleware

See Also

On this page