Adapters Overview โ
Asena uses a pluggable adapter system that allows you to choose the HTTP server implementation that best fits your needs. This architectural decision provides flexibility while maintaining a consistent API across all adapters.
What is an Adapter? โ
An adapter is a bridge between Asena's core framework and the underlying HTTP server implementation. It handles:
- HTTP request/response processing
- WebSocket connections
- Middleware execution
- Static file serving
- Context wrapping
Available Adapters โ
Asena currently provides two official adapters:
Zero runtime dependencies โ Zod is a peer your project owns. Routing is handed entirely to Bun's native router. Built for production APIs and microservices.
202k req/splaintext ยท measured on the published benchmarkHono Ecosystemhono and zod as peers your project owns. Middleware compatibility with the Hono ecosystem, ideal for gradual migration. Runs on Bun.
Identical application code runs on both โ only the adapter import changes. See Benchmarks for the full comparison across eleven scenarios.
Performance Comparison โ
Published numbers from the benchmark suite โ byte-verified workloads, wrk at 400 connections:
| Adapter | Runtime | Plaintext | JSON serialization | DB read by id |
|---|---|---|---|---|
| Ergenecore | Bun | 202,066 | 195,494 | 78,023 |
| Hono adapter | Bun | 190,030 | 178,857 | 76,780 |
| NestJS ยท Express | Bun | 131,281 | 119,195 | 29,178 |
| NestJS ยท Express | Node | 80,988 | 82,459 | 27,883 |
TIP
Full methodology โ hardware, load generator, isolation and the byte-level conformance gate โ is documented on the Benchmarks page.
Feature Comparison โ
| Feature | Ergenecore | Hono |
|---|---|---|
| HTTP Methods | โ | โ |
| WebSocket Support | โ | โ |
| Middleware System | โ | โ |
| Request Validation | โ (Zod) | โ (Zod) |
| Static File Serving | โ | โ |
| Cookie Support | โ | โ |
| CORS Middleware | โ | โ |
| Rate Limiting | โ | โ |
Choosing the Right Adapter โ
Use Ergenecore when: โ
- โ You need maximum performance
- โ You're building a Test or Poc project
- โ You want zero external dependencies
- โ You're using Bun runtime exclusively
- โ You want native Bun optimizations
Use Hono when: โ
- โ You're already familiar with Hono
- โ You're migrating an existing Hono project
- โ You need Hono-specific middleware
- โ You want a battle-tested adapter
Quick Start Comparison โ
Ergenecore Setup โ
import { AsenaServerFactory } from '@asenajs/asena';
import { createErgenecoreAdapter } from '@asenajs/ergenecore';
import { logger } from './logger';
const adapter = createErgenecoreAdapter();
const server = await AsenaServerFactory.create({
adapter,
logger,
port: 3000
});
await server.start();Hono Setup โ
import { AsenaServerFactory } from '@asenajs/asena';
import { createHonoAdapter } from '@asenajs/hono-adapter';
import { AsenaLogger } from '@asenajs/asena-logger';
// createHonoAdapter returns a tuple; createErgenecoreAdapter returns the adapter alone
const [adapter, logger] = createHonoAdapter({ logger: new AsenaLogger() });
const server = await AsenaServerFactory.create({
adapter,
logger,
port: 3000
});
await server.start();Context API โ
Both adapters implement the same AsenaContext interface, so handler code is identical - only the import path differs.
import type { Context } from '@asenajs/ergenecore';
// Get parameters - getParam is sync, getQuery/getBody are async
const id = context.getParam('id');
const page = await context.getQuery('page');
const body = await context.getBody<{ name: string }>();
// Send response
return context.send({ id, page, body }, 200);import type { Context } from '@asenajs/hono-adapter';
// Get parameters - getParam is sync, getQuery/getBody are async
const id = context.getParam('id');
const page = await context.getQuery('page');
const body = await context.getBody<{ name: string }>();
// Send response
return context.send({ id, page, body }, 200);The differences are in what context.req gives you: a native Request on Ergenecore, a HonoRequest on Hono. See Context API for the full surface.
Migration Between Adapters โ
TIP
Migrating between adapters means changing the adapter factory and the Context import path. Controllers, services and business logic stay unchanged.
From Hono to Ergenecore โ
import { Get } from '@asenajs/asena/decorators/http';
// Before: import type { Context } from '@asenajs/hono-adapter';
import type { Context } from '@asenajs/ergenecore';
// The handler body itself does not change
@Get('/:id')
async getUser(context: Context) {
const id = context.getParam('id');
return context.send({ id });
}The bootstrap file changes too, because the factories differ:
// Before (Hono) - returns a tuple
const [adapter, logger] = createHonoAdapter({ logger: new AsenaLogger() });
// After (Ergenecore) - returns the adapter alone
const adapter = createErgenecoreAdapter();Advanced Adapter Configuration โ
Ergenecore Advanced Setup โ
import { createErgenecoreAdapter } from '@asenajs/ergenecore';
const adapter = createErgenecoreAdapter({
hostname: '0.0.0.0',
enableWebSocket: true,
// Custom WebSocket adapter if needed
websocketAdapter: customWebSocketAdapter
});Hono Advanced Setup โ
import { createHonoAdapter } from '@asenajs/hono-adapter';
import { logger } from './logger';
// Single argument: either a bare logger, or an options object containing one
const [adapter, asenaLogger] = createHonoAdapter({
logger,
strict: false, // match '/health' and '/health/' alike - useful behind a reverse proxy
});Creating Custom Adapters โ
You can create your own adapter by implementing the AsenaAdapter interface:
import type { AsenaAdapter } from '@asenajs/asena/adapter';
export class MyCustomAdapter implements AsenaAdapter {
async start(port: number): Promise<void> {
// Implementation
}
registerRoute(method: string, path: string, handler: Function): void {
// Implementation
}
// ... implement other required methods
}TIP
Check the Ergenecore source code for a complete implementation example. or Check the Hono-adapter source code for a complete implementation example.
stop() has to reach your WebSocket layer
server.stop() calls the adapter's stop() before it runs any @OnStop hook, and an adapter with WebSocket support is responsible for tearing that layer down from there โ clearing heartbeat timers and calling the WebSocket transport's optional destroy().
Both official adapters do this now. Neither did before: destroy() had no call site anywhere in the framework, so a Redis-backed multi-pod setup leaked a subscriber and a publisher connection on every stop.
Recommendations โ
For New Projects โ
Start with Ergenecore for optimal performance and native Bun features.
bun add @asenajs/ergenecore zodFor Existing Hono Projects โ
Use the Hono adapter for seamless migration and reuse of existing middleware.
bun add @asenajs/hono-adapter hono zodFor Maximum Performance โ
Ergenecore provides:
- Native Bun optimizations
- Minimal dependency overhead
Related โ
- Ergenecore Adapter - Ergenecore features and API
- Hono Adapter - Hono adapter usage and API
- Context API - The adapter-agnostic request/response object
- Middleware Guide - Middleware across both adapters