Skip to content

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.

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:

typescript
import { Controller } from '@asenajs/asena/server';
import { Get, Post } from '@asenajs/asena/web';
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 = 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);
  }
}
typescript
import { Controller } from '@asenajs/asena/server';
import { Get, Post } from '@asenajs/asena/web';
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 = 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.

typescript
@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.

typescript
@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.

typescript
@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().

typescript
@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:

typescript
const id = Number(context.getParam('id'));

Query Parameters

Extract query string values using getQuery() and getQueryAll().

typescript
@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
  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.

typescript
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

typescript
try {
  const body = await context.getBody();
} catch (error) {
  return context.send({ error: 'Invalid JSON' }, 400);
}

Request Headers

Access specific headers using getHeader() (via native request object).

typescript
@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 });
}

Form Data

Parse multipart/form-data and URL-encoded forms.

typescript
@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).

typescript
@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.

typescript
@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.

typescript
@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.

typescript
@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).

typescript
@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' });
}

Retrieve cookie values, with optional signature verification.

typescript
@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.

typescript
@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 cookies with various options.

typescript
@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' });
}

Remove cookies by expiring them.

typescript
@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.

typescript
// In middleware
@Middleware()
export class AuthMiddleware extends MiddlewareService {
  async use(context: Context) {
    const token = context.req.headers.get('authorization');
    const userId = await verifyToken(token);

    // Store for later use
    context.setValue('userId', userId);
    context.setValue('isAdmin', userId === 'admin');
  }
}

Get Value - getValue()

Retrieve stored values in handlers.

typescript
@Get('/dashboard')
async dashboard(context: Context) {
  // Retrieve value set by middleware
  const userId = context.getValue<string>('userId');
  const isAdmin = context.getValue<boolean>('isAdmin');

  return context.send({ userId, isAdmin });
}

Type-Safe State

Use TypeScript generics for type-safe state access:

typescript
const userId = context.getValue<string>('userId');
const count = context.getValue<number>('count');

WebSocket Support

Context provides WebSocket-specific methods for upgrade handling.

Set WebSocket Value - setWebSocketValue()

Store data before WebSocket upgrade.

typescript
@Get('/ws')
async handleWebSocket(context: Context) {
  const userId = context.getValue('userId');

  // Store data for WebSocket handler
  context.setWebSocketValue({ userId, connectedAt: Date.now() });

  // WebSocket upgrade happens automatically
}

Get WebSocket Value - getWebSocketValue()

Retrieve data in WebSocket handlers.

typescript
@WebSocket('/ws')
export class ChatWebSocket {
  open(ws: ServerWebSocket) {
    const data = ws.data.context.getWebSocketValue<{
      userId: string;
      connectedAt: number;
    }>();

    console.log(`User ${data.userId} connected at ${data.connectedAt}`);
  }
}

Advanced Methods

Parse Body - getParseBody()

Automatically detect and parse request body based on Content-Type.

typescript
@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.

typescript
@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.

typescript
@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

typescript
// Middleware sets user data
@Middleware()
export class AuthMiddleware extends MiddlewareService {
  async use(context: Context) {
    const token = context.req.headers.get('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 });
  }
}

Pagination

typescript
@Get('/posts')
async listPosts(context: Context) {
  const page = Number(await context.getQuery('page')) || 1;
  const limit = Number(await context.getQuery('limit')) || 20;
  const offset = (page - 1) * limit;

  const posts = await this.postService.findAll({ offset, limit });
  const total = await this.postService.count();

  return context.send({
    posts,
    pagination: {
      page,
      limit,
      total,
      pages: Math.ceil(total / limit),
      hasNext: offset + limit < total,
      hasPrev: page > 1
    }
  });
}

Error Responses

typescript
@Get('/resource/:id')
async getResource(context: Context) {
  try {
    const id = Number(context.getParam('id'));

    if (isNaN(id)) {
      return context.send({
        error: 'Invalid ID format',
        code: 'INVALID_ID'
      }, 400);
    }

    const resource = await this.service.findById(id);

    if (!resource) {
      return context.send({
        error: 'Resource not found',
        code: 'NOT_FOUND'
      }, 404);
    }

    return context.send({ resource });
  } catch (error) {
    return context.send({
      error: 'Internal server error',
      code: 'INTERNAL_ERROR'
    }, 500);
  }
}

File Upload

typescript
@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);
}

Adapter-Specific Features

Ergenecore Adapter

Performance Optimizations:

  • Lazy URL parsing (only when query params accessed)
  • Lazy state Map (only when setValue/getValue called)
  • Body caching (allows multiple getBody() calls)

Native Bun Features:

  • Uses Bun's native cookie API
  • Direct access to Bun's Request/Response
typescript
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
  • Streaming response support
  • WebSocket via Hono's upgrade mechanism

Native Hono Context: Access Hono-specific features via the wrapped context.

typescript
import type { Context } from '@asenajs/hono-adapter';

@Get('/native')
async useNative(context: Context) {
  // Access Hono native methods
  const contentType = context.req.header('content-type');

  // Use Hono streaming (if needed)
  // Note: send() is still recommended for most cases

  return context.send({ framework: 'Hono' });
}

API Reference

Request Methods

MethodReturn TypeDescription
getParam(name)stringGet route parameter
getQuery(name)Promise<string>Get single query parameter
getQueryAll(name)Promise<string[]>Get all values of a query parameter
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

Response Methods

MethodReturn TypeDescription
send(data, status?)Response | Promise<Response>Send JSON response
html(html, status?)Response | Promise<Response>Send HTML response
redirect(url)voidRedirect to URL
MethodReturn TypeDescription
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

MethodReturn TypeDescription
getValue<T>(key)TGet context value
setValue(key, value)voidSet context value
getWebSocketValue<T>()TGet WebSocket data
setWebSocketValue(value)voidSet WebSocket data

Best Practices

Always Type Your Bodies

Use TypeScript generics for type-safe request bodies:

typescript
interface CreateUserDto {
  name: string;
  email: string;
}

const body = await context.getBody<CreateUserDto>();
// body.name and body.email are now type-safe

Use State for Middleware Communication

Share data between middlewares and handlers using setValue/getValue:

typescript
// Middleware
context.setValue('userId', extractedUserId);

// Handler
const userId = context.getValue<string>('userId');

Consistent Error Responses

Use a consistent error format across your API:

typescript
return context.send({
  error: 'Human-readable message',
  code: 'MACHINE_READABLE_CODE',
  details: {} // Optional
}, statusCode);

Async Methods

Most Context methods are async. Always use await:

typescript
// ❌ Wrong
const query = context.getQuery('q');

// ✅ Correct
const query = await context.getQuery('q');

Released under the MIT License.