Skip to content

Ergenecore Adapter

Ergenecore is Asena's native Bun adapter built exclusively with Bun's native APIs for maximum performance. Developed by the Asena team, it provides zero-dependency HTTP/WebSocket serving with SIMD-accelerated routing.

What is Ergenecore?

Ergenecore is a high-performance adapter that:

  • Built by Asena Team - First-party adapter maintained alongside Asena core
  • Bun-Native - Uses Bun.serve() and native Bun APIs exclusively
  • Zero Dependencies - no runtime dependencies at all; Zod is a peer your project owns
  • SIMD-Accelerated - Leverages Bun's SIMD-optimized routing engine
  • Zero-Copy File Serving - Uses Bun.file() for optimal static file performance
  • Built-in Middleware - Includes CorsMiddleware and RateLimiterMiddleware

Why Choose Ergenecore?

Performance

Ergenecore is the fastest Asena adapter:

AdapterRequests/secLatency (avg)
Ergenecore294,9621.34ms
Hono (standalone)266,4761.49ms
Hono adapter233,1821.70ms
NestJS (Bun)100,9753.92ms

Benchmark Details

12 threads, 400 connections, 120s duration

When to Use Ergenecore

Choose Ergenecore when:

  • ✅ You need maximum performance on Bun runtime
  • ✅ You want zero external dependencies
  • ✅ You need built-in CORS and rate limiting
  • ✅ You're building Bun-exclusive applications
  • ✅ You want first-party support and updates

Choose Hono Adapter when:

  • ✅ You need compatibility with Hono ecosystem
  • ✅ You're migrating from standalone Hono
  • ✅ You need Hono-specific middleware

INFO

For Hono adapter documentation, see Hono Adapter.

Installation

bash
bun add @asenajs/ergenecore zod

zod is a peer dependency: the adapter defines the validation contract, your project owns the library and its version.

Requirements:

  • Bun v1.3.12 or higher
  • @asenajs/asena v0.10.0 or higher
  • Zod v4.3.6 or higher (peer dependency)
  • TypeScript v5.9.3 or higher

Quick Start

Basic Server Setup

typescript
import { AsenaServerFactory } from '@asenajs/asena';
import { createErgenecoreAdapter } from '@asenajs/ergenecore';
import { logger } from './logger';

// Create adapter
const adapter = createErgenecoreAdapter();

// Create and start server
const server = await AsenaServerFactory.create({
  adapter,
  logger,
  port: 3000
});

await server.start();

Controller Example

typescript
import { Controller } from '@asenajs/asena/decorators';
import { Get, Post } from '@asenajs/asena/decorators/http';
import type { Context } from '@asenajs/ergenecore';

@Controller('/users')
export class UserController {
  @Get({ path: '/:id' })
  async getById(context: Context) {
    const id = context.getParam('id');
    return context.send({ id, name: 'John Doe' });
  }

  @Post({ path: '/' })
  async create(context: Context) {
    const body = await context.getBody();
    return context.send({ created: true, data: body }, 201);
  }
}

Context API

For complete Context API documentation, see Context.

Factory Functions

Ergenecore provides three factory functions for creating adapter instances with different configurations.

createErgenecoreAdapter(options?)

Creates a new Ergenecore adapter instance with custom configuration.

typescript
import { createErgenecoreAdapter } from '@asenajs/ergenecore';

const adapter = createErgenecoreAdapter({
  hostname: 'localhost',
  enableWebSocket: true,
  logger: customLogger
});

Options:

OptionTypeDefaultDescription
portnumberIgnored. AsenaServer.start() overwrites it with the port from AsenaServerFactory.create({ port }).
hostnamestringundefinedServer hostname. On Ergenecore this is the only way to set it - serveOptions.hostname is overwritten.
loggerServerLoggerundefinedCustom logger instance
enableWebSocketbooleantrueEnable WebSocket support
websocketAdapterErgenecoreWebsocketAdapterAutoCustom WebSocket adapter
logErrorsbooleantrueLog the failures the framework itself answers - a request your onError/onNotFound answered writes nothing. 5xx logs at error with a stack, 4xx at debug (falling back to info) without one, an unmatched route at info. Set false to silence all three. See Adapter logging.

createProductionAdapter(options?)

Creates a production-optimized adapter with sensible defaults.

typescript
import { createProductionAdapter } from '@asenajs/ergenecore';

const adapter = createProductionAdapter({
  hostname: '0.0.0.0',
  logger: productionLogger
});

Production Defaults:

  • WebSocket enabled (which is already the default)

It is an alias

createProductionAdapter(options) forwards to createErgenecoreAdapter(options) with enableWebSocket defaulted to true - and that is already the base default. There is no additional performance tuning; the name only documents intent.

createDevelopmentAdapter(options?)

Creates a development-friendly adapter with verbose logging.

typescript
import { createDevelopmentAdapter } from '@asenajs/ergenecore';

const adapter = createDevelopmentAdapter();

Development Defaults:

  • WebSocket forced on - unlike the other two factories, passing enableWebSocket: false here has no effect
  • Same default console logger as createErgenecoreAdapter

Prefer createErgenecoreAdapter

The three factories are near-identical. Use the plain one unless you specifically want the forced-WebSocket behaviour.

Built-in Middleware

Ergenecore includes two powerful built-in middleware classes that you can extend and customize.

CorsMiddleware

Ergenecore provides a high-performance CORS middleware with support for origin whitelisting and dynamic validation.

Basic CORS (Allow All Origins)

typescript
import { Middleware } from '@asenajs/asena/decorators';
import { CorsMiddleware } from '@asenajs/ergenecore';

@Middleware()
export class GlobalCors extends CorsMiddleware {
  constructor() {
    super(); // Defaults to { origin: '*' }
  }
}

Whitelist Specific Origins

typescript
@Middleware()
export class RestrictedCors extends CorsMiddleware {
  constructor() {
    super({
      origin: ['https://example.com', 'https://app.example.com'],
      credentials: true,
      methods: ['GET', 'POST', 'PUT', 'DELETE'],
      allowedHeaders: ['Content-Type', 'Authorization'],
      exposedHeaders: ['X-Total-Count'],
      maxAge: 86400 // 24 hours
    });
  }
}

Dynamic Origin Validation

typescript
@Middleware()
export class DynamicCors extends CorsMiddleware {
  constructor() {
    super({
      origin: (origin: string) => {
        // Allow all subdomains of example.com
        return origin.endsWith('.example.com') || origin === 'https://example.com';
      },
      credentials: true
    });
  }
}

CORS Options

OptionTypeDefaultDescription
origin'*' | string[] | (origin: string) => boolean'*'Allowed origins. A bare origin string is not supported here - wrap it in an array.
credentialsbooleanfalseAllow credentials
methodsstring[]['GET','POST','PUT','PATCH','DELETE','OPTIONS']Allowed HTTP methods
allowedHeadersstring[]['Content-Type', 'Authorization']Allowed request headers
exposedHeadersstring[][]Exposed response headers
maxAgenumber86400Preflight cache duration (sec)

Using CORS Middleware

typescript
import { Config } from '@asenajs/asena/decorators';
import { ConfigService } from '@asenajs/ergenecore';

// Global CORS
@Config()
export class ServerConfig extends ConfigService {
  globalMiddlewares() {
    return [GlobalCors];
  }
}

// Per-route CORS
@Controller('/api')
export class ApiController {
  @Get({ path: '/public', middlewares: [RestrictedCors] })
  async publicData(context: Context) {
    return context.send({ data: 'public' });
  }
}

Performance

CorsMiddleware uses lazy header allocation and pre-joined strings for optimal performance.

RateLimiterMiddleware

Ergenecore includes a Token Bucket-based rate limiter for controlling request rates and preventing abuse.

Basic Rate Limiter

typescript
import { Middleware } from '@asenajs/asena/decorators';
import { RateLimiterMiddleware } from '@asenajs/ergenecore';

// 100 requests per minute
@Middleware()
export class ApiRateLimiter extends RateLimiterMiddleware {
  constructor() {
    super({
      capacity: 100,
      refillRate: 100 / 60, // tokens per second
    });
  }
}

Strict Rate Limiter

typescript
// 5 requests per minute for sensitive endpoints
@Middleware()
export class StrictRateLimiter extends RateLimiterMiddleware {
  constructor() {
    super({
      capacity: 5,
      refillRate: 5 / 60,
      message: 'Too many login attempts. Please try again later.',
    });
  }
}

Advanced Rate Limiter

typescript
@Middleware()
export class CustomRateLimiter extends RateLimiterMiddleware {
  constructor() {
    super({
      capacity: 50,
      refillRate: 50 / 60,

      // Rate limit by user ID instead of IP
      keyGenerator: (ctx) => ctx.getValue('user')?.id || 'anonymous',

      // Skip rate limiting for admin users
      skip: (ctx) => ctx.getValue('user')?.role === 'admin',

      // Expensive operations cost more tokens
      cost: (ctx) => ctx.req.url.includes('/search') ? 5 : 1,

      // Custom response
      message: 'Rate limit exceeded. Please slow down.',
      statusCode: 429,

      // Cleanup settings
      cleanupInterval: 60000, // 1 minute
      bucketTTL: 600000 // 10 minutes
    });
  }
}

Rate Limiter Options

OptionTypeDefaultDescription
capacitynumber100Maximum burst capacity
refillRatenumber10Tokens per second
keyGenerator(ctx) => stringx-forwarded-forcf-connecting-ipgetRequestIp()'unknown'Client identifier function
messagestring'Rate limit exceeded...'Error message
statusCodenumber429HTTP status code
costnumber | (ctx) => number1Token cost per request
skip(ctx) => booleanundefinedSkip rate limiting function
cleanupIntervalnumber60000Cleanup interval (ms)
bucketTTLnumber600000Inactive bucket TTL (ms)

Rate Limit Headers

The middleware automatically sets these headers:

  • X-RateLimit-Limit: Requests allowed per minute
  • X-RateLimit-Remaining: Remaining tokens
  • X-RateLimit-Reset: Unix timestamp when bucket resets
  • Retry-After: Seconds to wait (on 429 response)

Using Rate Limiter

typescript
import { Config } from '@asenajs/asena/decorators';
import { ConfigService } from '@asenajs/ergenecore';

// Global rate limiter
@Config()
export class ServerConfig extends ConfigService {
  globalMiddlewares() {
    return [ApiRateLimiter];
  }
}

// Per-controller
@Controller('/api', { middlewares: [ApiRateLimiter] })
export class ApiController { }

// Per-route
@Controller('/auth')
export class AuthController {
  @Post({ path: '/login', middlewares: [StrictRateLimiter] })
  async login(context: Context) {
    const body = await context.getBody();
    return context.send({ token: 'abc123' });
  }
}

Token Bucket Algorithm

RateLimiterMiddleware uses O(1) bucket lookup and lazy token refill for optimal performance. Each middleware instance maintains its own bucket storage for route-specific rate limiting.

Its sweep timer is released on shutdown

RateLimiterMiddleware.destroy() carries an @OnStop, so server.stop() clears the cleanup interval and drops the bucket map. The hook is inherited, so your @Middleware() subclass gets it without redeclaring anything.

The timer was always unref()'d and never held the process open — what it did do is survive a stop/start cycle inside one process (an ordinary test suite does twenty), leaving a timer per stopped server still sweeping a map nobody reads, and letting a restarted server inherit rate-limit state from the one before it.

Performance & Architecture

SIMD-Accelerated Routing

Ergenecore leverages Bun's SIMD-accelerated router for ultra-fast route matching. No framework overhead, just native performance.

Zero-Copy File Serving

Uses Bun.file() for serving static files without copying data to memory. This provides optimal performance for static assets.

Bun-Native APIs

Ergenecore is built exclusively with:

  • Bun.serve() - Native HTTP server
  • Bun.file() - Zero-copy file I/O
  • Native WebSocket APIs
  • No external runtime dependencies

Ergenecore-Specific Features

Context Type

Always import Context from Ergenecore's types:

typescript
import type { Context } from '@asenajs/ergenecore';

INFO

For complete Context API, see Context.

Middleware Base Class

Extend MiddlewareService for custom middleware:

typescript
import { Middleware } from '@asenajs/asena/decorators';
import { MiddlewareService, type Context } from '@asenajs/ergenecore';

@Middleware()
export class AuthMiddleware extends MiddlewareService {
  async handle(context: Context, next: () => Promise<void>): Promise<any> {
    const token = context.headers['authorization'];
    if (!token) {
      return context.send({ error: 'Unauthorized' }, 401);
    }
    await next();
  }
}

INFO

For middleware patterns, see Middleware.

Validation Service

Extend ValidationService for request validation:

typescript
import { Middleware } from '@asenajs/asena/decorators';
import { ValidationService } from '@asenajs/ergenecore';
import { z } from 'zod';

@Middleware({ validator: true })
export class CreateUserValidator extends ValidationService {
  json() {
    return z.object({
      name: z.string().min(3),
      email: z.string().email()
    });
  }
}

INFO

For validation patterns, see Validation.

Config Service

Extend ConfigService for server configuration:

typescript
import { Config } from '@asenajs/asena/decorators';
import { ConfigService, type Context } from '@asenajs/ergenecore';
import type { NotFoundRequest } from '@asenajs/asena/adapter';

@Config()
export class ServerConfig extends ConfigService {
  globalMiddlewares() {
    return [GlobalCors, ApiRateLimiter];
  }

  onError(error: Error, context: Context): Response | Promise<Response> {
    return context.send({ error: 'Something went wrong' }, 500);
  }

  onNotFound(context: Context, request: NotFoundRequest): Response | Promise<Response> {
    return context.send({ title: 'Not Found', status: 404, instance: request.path }, 404);
  }
}

The two handlers do not overlap

onError is for something your code threw. onNotFound is for a request that matched no route — a routing outcome, not a failure — so neither handler has to ask which case it is looking at. An unmatched route never reaches onError.

request.path is the path only, with no origin and no query string, and request.method is normalised by the adapter, so the same handler body works unchanged on the Hono adapter. With no onNotFound declared, both adapters answer {"error":"Not Found"} with a 404.

A domain 404 — the route exists, the record does not — is still a throw, and still goes to onError:

typescript
import { HttpException } from '@asenajs/asena/adapter';

throw new HttpException(404, { error: 'User not found' });

HttpException lives in the framework core, not in this adapter, so the same throw works unchanged on the Hono adapter. @asenajs/ergenecore re-exports it under the name it has always had, and that re-export is the same class object.

onNotFound also catches missing static files

A file that @StaticServe cannot find reaches this hook too, so both adapters answer the same body. The per-route StaticServeService.onNotFound still runs first when you declare one.

Every thrown error reaches onError first

Including HttpException. The adapter used to answer an HttpException straight from getResponse() at several points and only consult your handler for everything else, so an application could reshape its own 4xx envelopes on the Hono adapter but not here. Your handler now sees all of them, and the adapter falls back to getResponse() (or a generic 500) only when there is no handler, when it returns nothing, or when it throws.

With no onError declared, an unhandled error answers {"error":"Internal Server Error"}. The thrown message is deliberately not echoed to the caller — it is written to the log with its stack instead.

INFO

For configuration, see Configuration, and for the full picture of both hooks see Error Handling.

Best Practices

1. Use Type Imports

typescript
// ✅ Good: Type-only import
import type { Context } from '@asenajs/ergenecore';

// ❌ Bad: Runtime import for types
import { Context } from '@asenajs/ergenecore';

2. Leverage Built-in Middleware

typescript
// ✅ Good: Use built-in CORS and rate limiting
@Config()
export class ServerConfig extends ConfigService {
  globalMiddlewares() {
    return [GlobalCors, ApiRateLimiter];
  }
}

3. Extend Ergenecore Base Classes

typescript
// ✅ Good: Extend base classes
import { Config, Middleware } from '@asenajs/asena/decorators';
import {
  ConfigService,
  MiddlewareService,
  ValidationService,
  type Context,
  type ValidationSchema,
} from '@asenajs/ergenecore';
import { isHttpException } from '@asenajs/asena/adapter';
import { z } from 'zod';

// MiddlewareService requires handle() - it is the only abstract member
@Middleware()
export class MyMiddleware extends MiddlewareService {
  public async handle(context: Context, next: () => Promise<void>) {
    await next();
  }
}

// ValidationService has no abstract members; define the request parts you validate
@Middleware({ validator: true })
export class MyValidator extends ValidationService {
  public json(): ValidationSchema {
    return z.object({ name: z.string() });
  }
}

// ConfigService has no abstract members; override only the hooks you need
@Config()
export class MyConfig extends ConfigService {
  public onError(error: Error, context: Context) {
    // Without this branch every deliberate 401/403/404 arrives at the client as a 500
    if (isHttpException(error)) {
      return error.getResponse?.() ?? context.send({ error: error.message }, error.status);
    }

    return context.send({ error: 'Internal Server Error' }, 500);
  }
}

4. Use Factory Functions

typescript
// ✅ Good: Use appropriate factory
const adapter = process.env.NODE_ENV === 'production'
  ? createProductionAdapter({ hostname: '0.0.0.0' })
  : createDevelopmentAdapter();

Troubleshooting

Common Issues

Issue: TypeScript errors with Context

typescript
// Solution: Use type-only import
import type { Context } from '@asenajs/ergenecore';

Issue: Middleware not executing

typescript
// Solution: Ensure middleware extends MiddlewareService
import { MiddlewareService } from '@asenajs/ergenecore';

@Middleware()
export class MyMiddleware extends MiddlewareService {
  async handle(context: Context, next: () => Promise<void>) {
    await next();
  }
}

Issue: WebSocket connection fails

typescript
// Solution: Enable WebSocket in adapter
const adapter = createErgenecoreAdapter({
  enableWebSocket: true
});

Next Steps:

Released under the MIT License.