Context API
The Context API is the heart of request/response handling in Asena. It provides a unified interface that works consistently across all adapters (Ergenecore and Hono), allowing you to write adapter-agnostic code.
The wrapper around the underlying request and response.
02req, res and the raw adapter objects.
Route params, query, body, headers and form data.
04JSON, text, HTML, redirects and status codes.
05Reading, setting and deleting cookies.
06Every request and response method in one table.
What is Context?
The Context object wraps the underlying HTTP request and response, providing convenient methods for:
- Extracting route parameters, query strings, and request bodies
- Sending JSON, HTML, or custom responses
- Managing cookies (with signing support)
- Storing per-request state
- Handling WebSocket upgrades
Adapter-Agnostic Design
The same Context API works identically in both Ergenecore and Hono adapters. Only the import path changes - your application code remains the same.
Quick Start
Here's how to use Context in your controllers with both adapters:
import { Controller } from '@asenajs/asena/decorators';
import { Get, Post } from '@asenajs/asena/decorators/http';
import type { Context } from '@asenajs/ergenecore';
@Controller('/api')
export class ApiController {
@Get('/user/:id')
async getUser(context: Context) {
const id = context.getParam('id');
const format = await context.getQuery('format');
return context.send({
userId: id,
format: format || 'json'
});
}
@Post('/user')
async createUser(context: Context) {
const body = await context.getBody<{ name: string; email: string }>();
return context.send({
created: true,
user: body
}, 201);
}
}import { Controller } from '@asenajs/asena/decorators';
import { Get, Post } from '@asenajs/asena/decorators/http';
import type { Context } from '@asenajs/hono-adapter';
@Controller('/api')
export class ApiController {
@Get('/user/:id')
async getUser(context: Context) {
const id = context.getParam('id');
const format = await context.getQuery('format');
return context.send({
userId: id,
format: format || 'json'
});
}
@Post('/user')
async createUser(context: Context) {
const body = await context.getBody<{ name: string; email: string }>();
return context.send({
created: true,
user: body
}, 201);
}
}Notice the Difference?
The only difference between the two examples is the import path for the Context type. The API is completely identical.
Core Properties
req - Request Object
Access the underlying request object for adapter-specific features.
@Get('/info')
async getInfo(context: Context) {
// Access native request
const url = context.req.url;
const method = context.req.method;
return context.send({ url, method });
}res - Response Object
Access the underlying response object to set headers directly.
@Get('/download')
async download(context: Context) {
// Set custom headers
context.res.headers.set('Content-Disposition', 'attachment; filename="data.json"');
context.res.headers.set('X-Custom-Header', 'value');
return context.send({ data: 'example' });
}headers - Request Headers
Get all request headers as a key-value object.
@Get('/headers')
async showHeaders(context: Context) {
const headers = context.headers;
return context.send({ headers });
}Request Data Methods
Route Parameters
Extract dynamic segments from the URL path using getParam().
@Get('/posts/:postId/comments/:commentId')
async getComment(context: Context) {
const postId = context.getParam('postId');
const commentId = context.getParam('commentId');
return context.send({ postId, commentId });
}Type Safety
Route parameters are always strings. Convert to numbers when needed:
const id = Number(context.getParam('id'));Query Parameters
Extract query string values using getQuery() and getQueryAll().
getQuery() returns Promise<string | undefined>. It distinguishes the two cases a query string can express:
| URL | await getQuery('page') |
|---|---|
/search | undefined — the parameter is absent |
/search?page= | '' — present but empty |
/search?page=2 | '2' |
@Get('/search')
async search(context: Context) {
// Single value: ?q=asena
const query = await context.getQuery('q');
// Multiple values: ?tags=node&tags=bun
const tags = await context.getQueryAll('tags');
// Optional with default - ?? keeps a deliberate empty value intact
const page = (await context.getQuery('page')) ?? '1';
const limit = (await context.getQuery('limit')) ?? '10';
return context.send({
query,
tags,
page: Number(page),
limit: Number(limit)
});
}Request Body
Parse JSON request bodies with automatic type casting.
interface CreateUserDto {
name: string;
email: string;
age: number;
}
@Post('/users')
async createUser(context: Context) {
// Type-safe body parsing
const body = await context.getBody<CreateUserDto>();
// body is now typed as CreateUserDto
console.log(body.name, body.email, body.age);
return context.send({ created: true, user: body }, 201);
}Empty Body Handling
Ergenecore: Empty request body returns {} (empty object)
Hono: Empty request body may throw an error - always handle parsing errors
try {
const body = await context.getBody();
} catch (error) {
return context.send({ error: 'Invalid JSON' }, 400);
}Request Headers
Access specific headers using req.headers (via native request object).
Hono returns headers from a function
Hono req.headers is a function that returns headers as values
const age:string = context.req.header().Age;but in the other hand ergenecore is just a regular records
const server:string = context.req.headers.get("X-Server")Here is example
@Get('/auth')
async checkAuth(context: Context) {
const token = context.req.headers.get('authorization');
const userAgent = context.req.headers.get('user-agent');
if (!token) {
return context.send({ error: 'Unauthorized' }, 401);
}
return context.send({ token, userAgent });
}@Get('/auth')
async checkAuth(context: Context) {
const token = context.req.header().Authorization;
const userAgent = context.req.header()["User-Agent"];
if (!token) {
return context.send({ error: 'Unauthorized' }, 401);
}
return context.send({ token, userAgent });
}Form Data
Parse multipart/form-data and URL-encoded forms.
@Post('/upload')
async handleUpload(context: Context) {
// Get form data
const formData = await context.getFormData();
const name = formData.get('name');
const file = formData.get('file'); // File object
return context.send({
name,
fileName: file instanceof File ? file.name : null
});
}Binary Data
Handle binary request bodies (ArrayBuffer, Blob).
@Post('/binary')
async handleBinary(context: Context) {
// Get as ArrayBuffer
const buffer = await context.getArrayBuffer();
// Or as Blob
const blob = await context.getBlob();
return context.send({ size: buffer.byteLength });
}Response Methods
JSON Response - send()
Send JSON responses with automatic content-type headers.
@Get('/data')
async getData(context: Context) {
// Simple JSON response (200 OK)
return context.send({ message: 'Success', data: [] });
}
@Post('/create')
async create(context: Context) {
// JSON with custom status code
return context.send({ created: true }, 201);
}
@Get('/error')
async error(context: Context) {
// Error response
return context.send({ error: 'Not found' }, 404);
}Custom Headers
Add custom headers to responses.
@Get('/with-headers')
async withHeaders(context: Context) {
return context.send(
{ data: 'example' },
{
status: 200,
headers: {
'X-Custom-Header': 'value',
'X-Request-ID': crypto.randomUUID()
}
}
);
}HTML Response - html()
Send HTML content with proper content-type.
@Get('/page')
async showPage(context: Context) {
const html = `
<!DOCTYPE html>
<html>
<head><title>Asena Page</title></head>
<body><h1>Hello from Asena!</h1></body>
</html>
`;
return context.html(html);
}Redirect - redirect()
Redirect to another URL (302 Found by default).
@Get('/old-path')
async oldPath(context: Context) {
return context.redirect('/new-path');
}
@Get('/login')
async login(context: Context) {
const isAuthenticated = context.getValue('authenticated');
if (isAuthenticated) {
return context.redirect('/dashboard');
}
return context.send({ message: 'Please login' });
}Cookie Management
Get Cookie - getCookie()
Retrieve cookie values, with optional signature verification.
@Get('/check-session')
async checkSession(context: Context) {
// Get simple cookie
const sessionId = await context.getCookie('session');
if (!sessionId) {
return context.send({ error: 'No session' }, 401);
}
return context.send({ sessionId });
}Signed Cookies
Use signed cookies for tamper-proof data.
@Post('/login')
async login(context: Context) {
const body = await context.getBody<{ username: string }>();
// Set signed cookie
await context.setCookie('userId', body.username, {
secret: 'your-secret-key',
extraOptions: {
httpOnly: true,
secure: true,
maxAge: 3600 // 1 hour
}
});
return context.send({ message: 'Logged in' });
}
@Get('/profile')
async profile(context: Context) {
// Verify signed cookie
const userId = await context.getCookie('userId', 'your-secret-key');
if (!userId) {
return context.send({ error: 'Invalid session' }, 401);
}
return context.send({ userId });
}Set Cookie - setCookie()
Set cookies with various options.
@Post('/preferences')
async setPreferences(context: Context) {
await context.setCookie('theme', 'dark', {
extraOptions: {
path: '/',
maxAge: 86400 * 30, // 30 days
httpOnly: false, // Accessible from JavaScript
sameSite: 'lax'
}
});
return context.send({ message: 'Preferences saved' });
}Delete Cookie - deleteCookie()
Remove cookies by expiring them.
@Post('/logout')
async logout(context: Context) {
await context.deleteCookie('session');
await context.deleteCookie('userId');
return context.send({ message: 'Logged out' });
}State Management
Context provides in-memory state storage for sharing data between middlewares and handlers.
Set Value - setValue()
Store per-request values.
// In middleware
@Middleware()
export class AuthMiddleware extends MiddlewareService {
async use(context: Context) {
const token = context.req.headers.get('authorization');
const user = await verifyToken(token);
// Store for later use
context.setValue('user', user);
}
}Get Value - getValue()
Retrieve stored values in handlers.
@Get('/dashboard')
async dashboard(context: Context) {
// Retrieve value set by middleware
const user = context.getValue('user');
return context.send({ user });
}Type-Safe Variables with AsenaVariables
For full type safety and IDE autocomplete, augment the AsenaVariables interface using TypeScript module augmentation:
// src/types/ContextState.ts
declare module '@asenajs/asena/adapter' {
interface AsenaVariables {
user: User;
requestId: string;
}
}
interface User {
id: string;
name: string;
}Now getValue() and setValue() are fully type-checked:
@Get('/profile')
async profile(context: Context) {
// Type-safe: TypeScript knows this returns User
const user = context.getValue('user');
// ^? User
// Type-safe: TypeScript enforces correct value type
context.setValue('requestId', crypto.randomUUID());
// ^? string (enforced)
return context.send({ id: user.id, name: user.name });
}IDE Autocomplete
With AsenaVariables augmented, your IDE will:
- Autocomplete key names in
getValue()andsetValue() - Show the correct return type for each key
- Flag type mismatches at compile time
You can still use generic types for dynamic keys: context.getValue<string>('dynamicKey')
Setup
Create a declaration file (e.g., src/types/ContextState.ts) and make sure it's included in your TypeScript compilation. The module path must be exactly '@asenajs/asena/adapter'.
WebSocket Support
Context provides WebSocket-specific methods for upgrade handling.
Set WebSocket Value - setWebSocketValue()
Store data before WebSocket upgrade.
import { Middleware } from '@asenajs/asena/decorators';
import type { Context, MiddlewareService } from '@asenajs/ergenecore';
@Middleware()
export class WsAuthMiddleware implements MiddlewareService {
async handle(context: Context, next: () => Promise<void>): Promise<boolean | Response> {
//.. Rest of code
context.setWebSocketValue({
userId: '123',
username: 'john_doe'
});
await next();
}
}Get WebSocket Value - getWebSocketValue()
Socket data is injected automatically
Socket data will automaticly injectining in ws.data.values by adapter. So you dont need to use this.
Streaming
Context provides three streaming methods for sending data progressively to the client. All methods work identically across Ergenecore and Hono adapters.
Server-Sent Events - streamSSE()
Send real-time events to the client using the SSE protocol. Automatically sets Content-Type: text/event-stream, Cache-Control: no-cache, and Connection: keep-alive.
@Get('/events')
async events(context: Context) {
return context.streamSSE(async (stream) => {
for (let i = 0; i < 5; i++) {
await stream.writeSSE({
data: JSON.stringify({ count: i, time: Date.now() }),
event: 'update',
id: String(i),
});
}
});
}SSEMessage Format
interface SSEMessage {
data?: string; // Event data (multi-line strings auto-split into separate data: lines)
comment?: string; // Comment lines, emitted as ": <line>" and invisible to EventSource
event?: string; // Event type name
id?: string; // Event ID for reconnection
retry?: number; // Reconnection time in milliseconds
}At least one of data and comment must be set — a frame carrying neither would say nothing at all, so writeSSE throws writeSSE: message needs data or comment.
Keep-alive comments
A comment is emitted as : <line> (one line per newline in the string), which the SSE specification defines as a no-op. EventSource clients never see it, so it does not fire an onmessage handler or advance the last-event-id — but it is traffic, which is exactly what keeps an idle connection from being closed by a proxy:
@Get('/live')
async live(context: Context) {
return context.streamSSE(async (stream) => {
while (!stream.aborted) {
await stream.writeSSE({ comment: 'ping' }); // writes ": ping\n\n"
await Bun.sleep(15_000);
}
});
}A message may carry both: the comment lines are written first, then the event.
A data heartbeat also works
writeSSE({ data: 'heartbeat', event: 'ping' }) holds the connection open just as well, but every client has to know about that event and filter it out. A comment asks nothing of the client.
Error Handling
@Get('/events')
async events(context: Context) {
return context.streamSSE(
async (stream) => {
// Main stream logic
await stream.writeSSE({ data: 'starting', event: 'status' });
await doWork();
await stream.writeSSE({ data: 'done', event: 'status' });
},
async (error, stream) => {
// Error handler — send error event to client
await stream.writeSSE({
data: JSON.stringify({ error: error.message }),
event: 'error',
});
},
);
}Client Disconnect Detection
@Get('/live')
async live(context: Context) {
return context.streamSSE(async (stream) => {
stream.onAbort(() => {
console.log('Client disconnected');
// Clean up resources
});
while (!stream.aborted) {
await stream.writeSSE({ data: 'heartbeat', event: 'ping' });
await Bun.sleep(1000);
}
});
}Text Stream - streamText()
Stream plain text content. Sets Content-Type: text/plain.
@Get('/generate')
async generate(context: Context) {
return context.streamText(async (stream) => {
const chunks = ['Hello', ' ', 'World', '!'];
for (const chunk of chunks) {
await stream.write(chunk);
}
});
}Generic Stream - stream()
Stream raw data without a predefined content type. Useful for binary data, CSV exports, or custom formats.
@Get('/export')
async export(context: Context) {
return context.stream(async (stream) => {
await stream.writeln('name,email,age');
await stream.writeln('John,john@example.com,30');
await stream.writeln('Jane,jane@example.com,25');
});
}Pipe a ReadableStream
@Get('/pipe')
async pipe(context: Context) {
return context.stream(async (stream) => {
const source = new ReadableStream({
start(controller) {
controller.enqueue(new TextEncoder().encode('piped content'));
controller.close();
},
});
await stream.pipe(source);
});
}StreamWriter API
All streaming methods provide a writer with these methods:
| Method | Parameters | Description |
|---|---|---|
write(input) | Uint8Array | string | Write raw data to the stream |
writeln(input) | string | Write a string followed by a newline |
close() | — | Close the stream normally |
pipe(body) | ReadableStream | Pipe a ReadableStream through the writer |
onAbort(listener) | () => void | Register a callback for client disconnection |
aborted | — | boolean — whether the client disconnected |
closed | — | boolean — whether the stream was closed normally |
The SSE stream writer (streamSSE) additionally provides:
| Method | Parameters | Description |
|---|---|---|
writeSSE(message) | SSEMessage | Write a formatted SSE message. Needs data, comment, or both — throws when given neither |
Auto-Close
Streams are automatically closed after the callback completes. You don't need to call stream.close() manually unless you want to close early.
Advanced Methods
Parse Body - getParseBody()
Automatically detect and parse request body based on Content-Type.
@Post('/auto-parse')
async autoParse(context: Context) {
// Handles JSON, form-data, and URL-encoded automatically
const body = await context.getParseBody();
return context.send({ parsed: body });
}Array Buffer - getArrayBuffer()
Get raw binary data as ArrayBuffer.
@Post('/process-image')
async processImage(context: Context) {
const buffer = await context.getArrayBuffer();
// Process binary data
const processed = await processImageBuffer(buffer);
return context.send({ size: buffer.byteLength, processed });
}Blob - getBlob()
Get request body as a Blob.
@Post('/upload-blob')
async uploadBlob(context: Context) {
const blob = await context.getBlob();
return context.send({
type: blob.type,
size: blob.size
});
}Common Patterns
Authentication Flow
// Middleware sets user data
@Middleware()
export class AuthMiddleware extends MiddlewareService {
async use(context: Context) {
const token = context.req.header().Authorization;
if (!token) {
throw new Error('Unauthorized');
}
const user = await this.verifyToken(token);
context.setValue('user', user);
}
}
// Controller uses user data
@Controller({ path: '/api', middlewares: [AuthMiddleware] })
export class ApiController {
@Get('/profile')
async getProfile(context: Context) {
const user = context.getValue('user');
return context.send({ user });
}
}File Upload
@Post('/upload')
async uploadFile(context: Context) {
const formData = await context.getFormData();
const file = formData.get('file');
const description = formData.get('description');
if (!file || !(file instanceof File)) {
return context.send({ error: 'No file provided' }, 400);
}
// Process file
const buffer = await file.arrayBuffer();
const saved = await this.fileService.save(file.name, buffer);
return context.send({
uploaded: true,
fileName: file.name,
size: file.size,
description
}, 201);
}Utility Methods
Get Client IP - getRequestIp()
Get the client's IP address. Lazily evaluated and cached — zero cost if never accessed.
@Get('/info')
async info(context: Context) {
const ip = context.getRequestIp();
return context.send({ ip });
}Get All Queries - getAllQueries()
Get all query parameters as a key-value object. Keys with multiple values return arrays.
// GET /search?q=asena&tag=bun&tag=typescript
@Get('/search')
async search(context: Context) {
const queries = context.getAllQueries();
// { q: 'asena', tag: ['bun', 'typescript'] }
return context.send(queries);
}Response Headers - setResponseHeader() and appendResponseHeader()
Both write a header that is merged into the final response — useful in middleware for headers that must carry through to streaming responses. They differ in what happens to a value that is already there:
| Method | Existing value |
|---|---|
setResponseHeader(key, value) | Replaced. The last write wins. |
appendResponseHeader(key, value) | Kept. The new value is added alongside it. |
Set is the right default: most headers hold exactly one value, and a second Content-Type or Cache-Control is a bug, not a list.
@Get('/download')
async download(context: Context) {
context.setResponseHeader('Content-Disposition', 'attachment; filename="data.csv"');
context.setResponseHeader('X-Request-Id', crypto.randomUUID());
return context.send({ data: 'example' });
}Append is for the headers that genuinely are lists — Vary, Link, Accept-Encoding — where clobbering what an upstream middleware wrote is a real bug. A CORS middleware that needs Vary: Origin must not drop an upstream Vary: Accept-Encoding:
context.appendResponseHeader('Vary', 'Origin');
// upstream had "Vary: Accept-Encoding" -> "Vary: Accept-Encoding, Origin"Not for Set-Cookie
Cookies go through setCookie(), never through either method. Set-Cookie is the one header that must repeat rather than comma-join, and on Ergenecore appendResponseHeader comma-joins.
Adapter-Specific Features
Ergenecore Adapter
Performance Optimizations:
- Lazy URL parsing (only when query params accessed)
- Lazy state Map (only when setValue/getValue called)
- Lazy IP resolution (only when getRequestIp() called)
- Body caching (allows multiple getBody() calls)
Native Bun Features:
- Uses Bun's native cookie API
- Direct access to Bun's Request/Response
import type { Context } from '@asenajs/ergenecore';
@Get('/native')
async useNative(context: Context) {
// Access Bun native request
const bunRequest: Request = context.req;
return context.send({ framework: 'Ergenecore' });
}Hono Adapter
Rich Ecosystem:
- Full access to Hono's middleware ecosystem
@Overridedecorator for native Hono middleware- WebSocket via Hono's upgrade mechanism
Native Hono Context: Access Hono-specific features via the wrapped context.
import type { Context } from '@asenajs/hono-adapter';
@Get('/native')
async useNative(context: Context) {
// Access Hono native methods
const contentType = context.req.header('content-type');
return context.send({ framework: 'Hono' });
}API Reference
Request Methods
| Method | Return Type | Description |
|---|---|---|
getParam(name) | string | Get route parameter |
getQuery(name) | Promise<string | undefined> | Get single query parameter — undefined when absent, '' when present but empty |
getQueryAll(name) | Promise<string[]> | Get all values of a query parameter |
getAllQueries() | Record<string, string | string[]> | Get all query parameters as object |
getBody<T>() | Promise<T> | Parse JSON body with type |
getParseBody() | Promise<any> | Auto-parse body by content-type |
getFormData() | Promise<FormData> | Parse form data |
getArrayBuffer() | Promise<ArrayBuffer> | Get binary body |
getBlob() | Promise<Blob> | Get body as Blob |
getRequestIp() | string | null | Get client IP address (lazy, cached) |
Response Methods
| Method | Return Type | Description |
|---|---|---|
send(data, statusOrOptions?) | Response | Send JSON/text response |
html(data, statusOrOptions?) | Response | Send HTML response |
redirect(url) | Response | Redirect to URL |
setResponseHeader(key, value) | void | Set a response header, replacing any value already set for it |
appendResponseHeader(key, value) | void | Append to a response header, keeping existing values (Vary, Link, …) |
Streaming Methods
| Method | Return Type | Description |
|---|---|---|
stream(cb, onError?) | Response | Generic binary/text stream |
streamSSE(cb, onError?) | Response | Server-Sent Events stream (text/event-stream) |
streamText(cb, onError?) | Response | Text stream (text/plain) |
Cookie Methods
| Method | Return Type | Description |
|---|---|---|
getCookie(name, secret?) | Promise<string | false> | Get cookie value |
setCookie(name, value, options?) | Promise<void> | Set cookie |
deleteCookie(name, options?) | Promise<void> | Delete cookie |
State Methods
| Method | Return Type | Description |
|---|---|---|
getValue<K>(key) | AsenaVariables[K] | Get context value (type-safe with AsenaVariables) |
setValue<K>(key, value) | void | Set context value (type-safe with AsenaVariables) |
getWebSocketValue<T>() | T | Get WebSocket data |
setWebSocketValue(value) | void | Set WebSocket data |
Best Practices
Always Type Your Bodies
Use TypeScript generics for type-safe request bodies:
interface CreateUserDto {
name: string;
email: string;
}
const body = await context.getBody<CreateUserDto>();
// body.name and body.email are now type-safeUse State for Middleware Communication
Share data between middlewares and handlers using setValue/getValue:
// Middleware
context.setValue('userId', extractedUserId);
// Handler
const userId = context.getValue<string>('userId');Consistent Error Responses
Use a consistent error format across your API:
return context.send({
error: 'Human-readable message',
code: 'MACHINE_READABLE_CODE',
details: {} // Optional
}, statusCode);Async Methods
getBody(), getQuery(), getQueryAll(), getFormData(), getParseBody(), getArrayBuffer(), getBlob() and the cookie helpers return a Promise. Forgetting await does not raise a type error in every position - it silently yields the Promise object instead of the value:
// ❌ Wrong - `page` is a Promise, so `?? '1'` never applies and Number() gives NaN
const page = context.getQuery('page') ?? '1';
// ✅ Correct
const page = (await context.getQuery('page')) ?? '1';getParam(), getAllQueries(), getValue(), setValue() and send() are synchronous.
Related
- Controllers - Using Context in controllers
- Middleware - Context in middlewares
- WebSocket - WebSocket integration with Context
- Error Handling - HttpException and error responses
- Ergenecore Adapter - Ergenecore-specific features
- Hono Adapter - Hono-specific features