Skip to content

Decorators API

Package: @onebun/core

Module Decorators

@Module()

Defines a module that groups controllers, services, and imports. See Architecture — Module System for concepts and lifecycle details.

typescript
@Module(options: ModuleOptions)

ModuleOptions:

typescript
interface ModuleOptions {
  /** Other modules to import (their exported services become available) */
  imports?: Function[];

  /** Controller classes to register */
  controllers?: Function[];

  /** Service classes to register as providers */
  providers?: unknown[];

  /** Services to export to parent modules */
  exports?: unknown[];
}

Example:

typescript
import { Module } from '@onebun/core';
import { CacheModule } from '@onebun/cache';
import { UserController } from './user.controller';
import { UserService } from './user.service';

@Module({
  imports: [CacheModule],
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

exports accepts services only. Listing a MODULE there — the NestJS re-export idiom — throws OneBunInvalidExportError naming both modules. It never worked: a re-exported module contributed nothing to the importer, so either the importer failed at a controller with no mention of the export, or it also imported the module directly and got a second copy of every provider. Import the module that provides the service directly wherever the service is needed:

typescript
// Does not work — throws OneBunInvalidExportError at boot
@Module({ imports: [CoreModule], exports: [CoreModule] })
export class FeatureModule {}

// Import the providing module where the service is needed
@Module({ imports: [CoreModule], providers: [UserService] })
export class FeatureModule {}

@Global()

Marks a module as global. Global modules export their providers to all modules automatically without explicit import. This is useful for modules that provide cross-cutting concerns like database access or caching.

typescript
@Global()

Example:

typescript
import { Module, Global } from '@onebun/core';
import { DatabaseService } from './database.service';

@Global()
@Module({
  providers: [DatabaseService],
  exports: [DatabaseService],
})
export class DatabaseModule {}

// Now DatabaseService is available in ALL modules without importing DatabaseModule
@Module({
  controllers: [UserController],
  providers: [UserService], // UserService can inject DatabaseService
})
export class UserModule {}

Related Functions:

typescript
// Check if a module is global
function isGlobalModule(target: Function): boolean;

// Remove module from global registry (used internally)
function removeFromGlobalModules(target: Function): void;

Controller Decorators

@Controller()

Marks a class as an HTTP controller with a base path.

typescript
@Controller(basePath?: string)

Example:

typescript
import { Controller, BaseController } from '@onebun/core';

@Controller('/api/users')
export class UserController extends BaseController {
  // All routes will be prefixed with /api/users
}

Note: Leading slash is optional. @Controller('api/users') and @Controller('/api/users') are equivalent — the framework normalizes the path automatically.

HTTP Method Decorators

@Get(), @Post(), @Put(), @Delete(), @Patch(), @Options(), @Head(), @All()

Define HTTP endpoints on controller methods.

typescript
@Get(path?: string, options?: RouteOptions)
@Post(path?: string, options?: RouteOptions)
@Put(path?: string, options?: RouteOptions)
@Delete(path?: string, options?: RouteOptions)
@Patch(path?: string, options?: RouteOptions)
@Options(path?: string, options?: RouteOptions)
@Head(path?: string, options?: RouteOptions)
@All(path?: string, options?: RouteOptions)

interface RouteOptions {
  /** Per-request idle timeout in seconds. Set to 0 to disable. */
  timeout?: number;
}

Note: The leading slash in the path argument is optional. @Get('users') and @Get('/users') are equivalent. Similarly, @Get(':id') is the same as @Get('/:id'). This makes migration from NestJS seamless — NestJS-style paths work out of the box.

Per-request timeout:

Override the global idleTimeout for individual routes. Useful for long-running endpoints:

typescript
@Controller('/tasks')
export class TaskController extends BaseController {
  @Post('/process', { timeout: 300 }) // 5 minutes for this endpoint
  async processTask(@Body() body: unknown) {
    // long-running task...
  }

  @Get('/export', { timeout: 0 }) // no timeout
  async exportAll() {
    // very long export...
  }
}

Path Parameters:

Use :paramName syntax for dynamic segments:

typescript
@Controller('/users')
export class UserController extends BaseController {
  @Get('/')              // GET /users
  async findAll() {}

  @Get('/:id')           // GET /users/123
  async findOne(@Param('id') id: string) {}

  @Get('/:userId/posts') // GET /users/123/posts
  async getUserPosts(@Param('userId') userId: string) {}

  @Post('/')             // POST /users
  async create(@Body() body: CreateUserDto) {}

  @Put('/:id')           // PUT /users/123
  async update(@Param('id') id: string, @Body() body: UpdateUserDto) {}

  @Delete('/:id')        // DELETE /users/123
  async remove(@Param('id') id: string) {}
}

@All() — catch-all routes

@All() is a true catch-all, like NestJS router.all(): the decorated handler answers every HTTP method on that path, not just the seven with a decorator of their own. That includes OPTIONS and HEAD, and it includes methods the framework has no decorator for at all — PROPFIND, PURGE, LOCK, QUERY, vendor verbs. This is what makes @All() usable for reverse proxies, webhook receivers and legacy-path shims.

typescript
@Controller('/gateway')
export class GatewayController extends BaseController {
  @All('/proxy/:id')
  async proxy(@Param('id') id: string, @Req() req: OneBunRequest) {
    // req.method is whatever the client sent: GET, POST, PROPFIND, QUERY, ...
    return { id, method: req.method };
  }
}

Precedence — an explicitly declared verb always wins. If the same path carries both @All() and a concrete verb decorator, the concrete one handles its verb and @All() handles everything else. The rule is fixed; it does not depend on the order the decorators are written in:

typescript
@Controller('/webhooks')
export class WebhookController extends BaseController {
  @All('/github')
  async fallback() {
    return { handled: 'all' };   // PUT, DELETE, PROPFIND, ... land here
  }

  @Get('/github')
  async health() {
    return { handled: 'get' };   // GET lands here, not in fallback()
  }
}

CORS preflight. Because an @All() path claims OPTIONS, a preflight request reaches that route's middleware chain. When the application is configured with cors, the built-in CorsMiddleware runs first and answers the preflight with 204 — the @All() handler body never runs. Without cors configured, the @All() handler itself answers OPTIONS.

OpenAPI. An @All() route has no single HTTP method, so it cannot be expressed as one OpenAPI operation. Document the individual verbs you care about with concrete decorators, or exclude the route from the generated spec.

Parameter Decorators

All parameter decorators support an options object to control whether the parameter is required:

typescript
interface ParamDecoratorOptions {
  required?: boolean;
}

@Param()

Extract path parameter from URL. Path parameters are always required per OpenAPI specification.

typescript
@Param(name: string, schema?: Type<unknown>)

Example:

typescript
import { type } from '@onebun/core';

const idSchema = type('string.uuid');

@Get('/:id')
async findOne(
  @Param('id') id: string,                    // Always required (OpenAPI spec)
  @Param('id', idSchema) id: string,          // With validation, always required
) {}

@Query()

Extract query parameter from URL. Optional by default.

typescript
@Query(name: string, options?: ParamDecoratorOptions)
@Query(name: string, schema?: Type<unknown>, options?: ParamDecoratorOptions)

Example:

typescript
// GET /users?page=1&limit=10
@Get('/')
async findAll(
  @Query('page') page?: string,                        // Optional (default)
  @Query('limit', { required: true }) limit: string,   // Explicitly required
) {}

// With validation schema
@Get('/search')
async search(
  @Query('q', type('string')) query?: string,                      // Optional with validation
  @Query('sort', type('string'), { required: true }) sort: string, // Required with validation
) {}

@Body()

Extract and optionally validate request body. Required is determined from schema - if the schema accepts undefined, the body is optional; otherwise it's required.

typescript
@Body(schema?: Type<unknown>, options?: ParamDecoratorOptions)

Example:

typescript
import { type } from '@onebun/core';

const createUserSchema = type({
  name: 'string',
  email: 'string.email',
  'age?': 'number > 0',
});
type CreateUserBody = typeof createUserSchema.infer;

// Schema doesn't accept undefined → required
@Post('/')
async create(
  @Body(createUserSchema) body: CreateUserBody,
) {}

// Schema accepts undefined → optional
const optionalBodySchema = type({
  name: 'string',
}).or(type.undefined);
type OptionalBody = typeof optionalBodySchema.infer;

@Post('/optional')
async createOptional(
  @Body(optionalBodySchema) body: OptionalBody,
) {}

// Explicit override
@Post('/force-optional')
async forceOptional(
  @Body(createUserSchema, { required: false }) body: CreateUserBody,
) {}

// Without validation - body is unknown
@Post('/simple')
async createSimple(
  @Body() body: unknown,
) {}

Extract header value. Optional by default.

typescript
@Header(name: string, options?: ParamDecoratorOptions)
@Header(name: string, schema?: Type<unknown>, options?: ParamDecoratorOptions)

Example:

typescript
@Get('/protected')
async protected(
  @Header('X-Request-ID') requestId?: string,                      // Optional (default)
  @Header('Authorization', { required: true }) auth: string,       // Explicitly required
) {}

// With validation schema
@Get('/api')
async api(
  @Header('X-API-Key', type('string'), { required: true }) apiKey: string,
) {}

Extract cookie value from request. Uses BunRequest.cookies (CookieMap) under the hood. Optional by default.

typescript
@Cookie(name: string, options?: ParamDecoratorOptions)
@Cookie(name: string, schema?: Type<unknown>, options?: ParamDecoratorOptions)

Example:

typescript
// GET /api/me (with Cookie: session=abc123; theme=dark)
@Get('/me')
async getMe(
  @Cookie('session') session?: string,                       // Optional (default)
  @Cookie('session', { required: true }) session: string,    // Explicitly required
) {}

// With validation schema
@Get('/prefs')
async prefs(
  @Cookie('theme', type('"light" | "dark"')) theme?: string,  // Optional with validation
) {}

@Req()

Inject the raw request object. The type is OneBunRequest (alias for BunRequest), which extends the standard Web API Request with:

  • .cookies — a CookieMap for reading and setting cookies
  • .params — route parameters extracted by Bun's routes API
typescript
@Req()

Example:

typescript
import type { OneBunRequest } from '@onebun/core';

@Get('/raw')
async handleRaw(@Req() req: OneBunRequest) {
  const url = new URL(req.url);
  const headers = Object.fromEntries(req.headers);

  // Access cookies via CookieMap
  const session = req.cookies.get('session');

  // Access route params (populated by Bun routes API)
  // For route '/users/:id', req.params.id is available
}

@Res() (deprecated)

Deprecated

@Res() is deprecated and currently injects undefined. Use return new Response(...) from your handler instead. Direct response manipulation is not supported — return a Response object to set custom headers, status codes, and cookies.

typescript
@Res()

File Upload Decorators

Decorators for handling file uploads via multipart/form-data or JSON with base64-encoded data. The framework auto-detects the content type and provides a unified OneBunFile object.

@UploadedFile()

Extracts a single file from the request. Required by default.

typescript
@UploadedFile(fieldName?: string, options?: FileUploadOptions)

FileUploadOptions:

typescript
interface FileUploadOptions {
  /** Maximum file size in bytes */
  maxSize?: number;
  /** Allowed MIME types, supports wildcards like 'image/*'. Use MimeType enum. */
  mimeTypes?: string[];
  /** Whether the file is required (default: true) */
  required?: boolean;
}

Example:

typescript
import { Controller, Post, UploadedFile, MimeType, OneBunFile, BaseController } from '@onebun/core';

@Controller('/api/files')
export class FileController extends BaseController {
  @Post('/avatar')
  async uploadAvatar(
    @UploadedFile('avatar', {
      maxSize: 5 * 1024 * 1024,
      mimeTypes: [MimeType.ANY_IMAGE],
    }) file: OneBunFile,
  ) {
    await file.writeTo(`./uploads/${file.name}`);
    return { filename: file.name, size: file.size };
  }
}

@UploadedFiles()

Extracts multiple files from the request. Required by default (at least one file expected).

typescript
@UploadedFiles(fieldName?: string, options?: FilesUploadOptions)

FilesUploadOptions:

typescript
interface FilesUploadOptions extends FileUploadOptions {
  /** Maximum number of files allowed */
  maxCount?: number;
}

Example:

typescript
@Post('/documents')
async uploadDocs(
  @UploadedFiles('docs', { maxCount: 10 }) files: OneBunFile[],
) {
  for (const file of files) {
    await file.writeTo(`./uploads/${file.name}`);
  }
  return { count: files.length };
}

// All files from request (no field name filter)
@Post('/batch')
async uploadBatch(
  @UploadedFiles(undefined, { maxCount: 20 }) files: OneBunFile[],
) {
  return { count: files.length };
}

@FormField()

Extracts a non-file form field from the request. Optional by default.

typescript
@FormField(fieldName: string, options?: ParamDecoratorOptions)

Example:

typescript
@Post('/profile')
async createProfile(
  @UploadedFile('avatar', { mimeTypes: [MimeType.ANY_IMAGE] }) avatar: OneBunFile,
  @FormField('name', { required: true }) name: string,
  @FormField('email') email: string,
) {
  await avatar.writeTo(`./uploads/${avatar.name}`);
  return { name, email, avatar: avatar.name };
}

OneBunFile

Unified file wrapper returned by @UploadedFile and @UploadedFiles. Works the same regardless of upload method (multipart or JSON+base64).

typescript
class OneBunFile {
  readonly name: string;          // File name
  readonly size: number;          // File size in bytes
  readonly type: string;          // MIME type
  readonly lastModified: number;  // Last modified timestamp

  async toBase64(): Promise<string>;        // Convert to base64 string
  async toBuffer(): Promise<Buffer>;        // Convert to Buffer
  async toArrayBuffer(): Promise<ArrayBuffer>; // Convert to ArrayBuffer
  toBlob(): Blob;                           // Get underlying Blob
  async writeTo(path: string): Promise<void>; // Write to disk

  static fromBase64(data: string, filename?: string, mimeType?: string): OneBunFile;
}

MimeType Enum

Common MIME types for use with file upload options:

typescript
import { MimeType } from '@onebun/core';

// Wildcards
MimeType.ANY           // '*/*'
MimeType.ANY_IMAGE     // 'image/*'
MimeType.ANY_VIDEO     // 'video/*'
MimeType.ANY_AUDIO     // 'audio/*'

// Images
MimeType.PNG, MimeType.JPEG, MimeType.GIF, MimeType.WEBP, MimeType.SVG

// Documents
MimeType.PDF, MimeType.JSON, MimeType.XML, MimeType.ZIP, MimeType.CSV, MimeType.XLSX, MimeType.DOCX

// Video/Audio
MimeType.MP4, MimeType.WEBM, MimeType.MP3, MimeType.WAV

// Text
MimeType.PLAIN, MimeType.HTML, MimeType.CSS, MimeType.JAVASCRIPT

// Binary
MimeType.OCTET_STREAM

JSON Base64 Upload Format

When sending files via application/json, the framework accepts two formats:

typescript
// Full format with metadata
{ "avatar": { "data": "iVBORw0KGgo...", "filename": "photo.png", "mimeType": "image/png" } }

// Simplified format (raw base64 string)
{ "avatar": "iVBORw0KGgo..." }

The same @UploadedFile decorator works for both multipart and JSON uploads.

WARNING

@Body() cannot be used together with @UploadedFile, @UploadedFiles, or @FormField on the same method, since both consume the request body.

Service Decorators

@Service()

Marks a class as an injectable service.

typescript
@Service(tag?: Context.Tag<T, T>)

Example:

typescript
import { Service, BaseService } from '@onebun/core';

@Service()
export class UserService extends BaseService {
  // Service with auto-generated tag

  async findAll(): Promise<User[]> {
    this.logger.info('Finding all users');
    // ...
  }
}

// With custom Effect.js tag
import { Context } from 'effect';

const CustomServiceTag = Context.GenericTag<CustomService>('CustomService');

@Service(CustomServiceTag)
export class CustomService extends BaseService {
  // Service with explicit tag
}

@Inject()

Explicit dependency injection for edge cases. In most cases, automatic DI works without this decorator. See Architecture — Dependency Injection for how DI resolution works.

typescript
@Inject(type: new (...args: any[]) => T)
@Inject(token: symbol | string)

When to use @Inject:

  • Interface or abstract class injection
  • Token-based injection (custom Context.Tag)
  • Overriding automatic resolution
  • Picking WHICH named registration a parameter gets, in a module that selected two of them

Example:

typescript
@Controller('/users')
export class UserController extends BaseController {
  constructor(
    // Automatic injection (works in most cases) - no @Inject needed
    private userService: UserService,
    private cacheService: CacheService,

    // @Inject needed only for edge cases:
    // - When injecting by interface instead of concrete class
    // - When using custom Effect.js Context.Tag
    @Inject(SomeAbstractService) private abstractService: SomeAbstractService,
  ) {
    super();
  }
}

Selecting a named registration:

Only needed in a module that imported TWO registrations of one service. A module that selected one resolves it by type, with no annotation at all.

typescript
@Module({
  imports: [
    DrizzleModule.forFeature(MAIN_DB),
    DrizzleModule.forFeature(ANALYTICS_DB),
  ],
  providers: [Reconciler],
})
export class ReconcileModule {}

@Service()
export class Reconciler extends BaseService {
  constructor(
    @Inject(MAIN_DB) private main: DrizzleService,
    @Inject(ANALYTICS_DB) private analytics: DrizzleService,
    private clock: ClockService,          // un-annotated parameters resolve normally
  ) {
    super();
  }
}

Asking for a token the module never selected throws at startup, naming what it did select — the alternative is being handed the other database, silently.

@Optional()

Marks a constructor parameter as optional for dependency injection. When the dependency cannot be resolved, undefined is injected instead of throwing DependencyResolutionError.

typescript
@Optional()

When to use @Optional:

  • Feature-gated dependencies (e.g., caching that may not be configured)
  • Graceful degradation when an optional module is not imported

Example:

typescript
@Service()
export class NotificationService extends BaseService {
  constructor(
    private userService: UserService,                  // required — throws if missing
    @Optional() private emailService?: EmailService,   // optional — undefined if missing
  ) {
    super();
  }

  async notify(userId: string, message: string) {
    if (this.emailService) {
      await this.emailService.send(userId, message);
    } else {
      this.logger.warn('EmailService not available, skipping email notification');
    }
  }
}

Middleware Decorators

@Middleware()

Class decorator for middleware. Apply it to classes that extend BaseMiddleware so that the framework can resolve constructor dependencies automatically (TypeScript emits design:paramtypes when the class has a decorator). Without @Middleware(), you would need @Inject() on each constructor parameter for DI to work.

typescript
@Middleware()
class AuthMiddleware extends BaseMiddleware {
  constructor(private authService: AuthService) {
    super();
  }
  async use(req, next) { ... }
}

@UseMiddleware()

Apply middleware to a route handler or to all routes in a controller. Works as both a method decorator and a class decorator. Pass class constructors extending BaseMiddleware (not instances).

typescript
// Method decorator — applies to a single route
@UseMiddleware(...middleware: MiddlewareClass[])

// Class decorator — applies to every route in the controller
@UseMiddleware(...middleware: MiddlewareClass[])

Method-level example:

typescript
import { BaseMiddleware, type OneBunRequest, type OneBunResponse } from '@onebun/core';

class AuthMiddleware extends BaseMiddleware {
  async use(req: OneBunRequest, next: () => Promise<OneBunResponse>) {
    const token = req.headers.get('Authorization');
    if (!token) {
      return new Response('Unauthorized', { status: 401 });
    }
    return next();
  }
}

class LogMiddleware extends BaseMiddleware {
  async use(req: OneBunRequest, next: () => Promise<OneBunResponse>) {
    this.logger.info(`${req.method} ${req.url}`);
    return next();
  }
}

@Controller('/users')
export class UserController extends BaseController {
  @Get('/protected')
  @UseMiddleware(AuthMiddleware)
  async protectedRoute() {
    return { message: 'Secret data' };
  }

  @Post('/action')
  @UseMiddleware(LogMiddleware, AuthMiddleware)  // Multiple middleware
  async action() {
    return { message: 'Action performed' };
  }
}

Class-level example:

typescript
@Controller('/admin')
@UseMiddleware(AuthMiddleware)  // Applied to ALL routes in this controller
export class AdminController extends BaseController {
  @Get('/dashboard')
  getDashboard() {
    return { stats: {} };
  }

  @Put('/settings')
  @UseMiddleware(AuditLogMiddleware)  // Additional middleware for this route
  updateSettings() {
    return { updated: true };
  }
}

When both class-level and method-level middleware are present, execution order is: controller-level -> route-level -> handler.

Middleware classes support full DI through the constructor. Use @Middleware() on the class for automatic dependency resolution. See Controllers API — Middleware for details and examples.

Response Decorators

@ApiResponse()

Define response schema for documentation and validation.

typescript
@ApiResponse(statusCode: number, options?: {
  schema?: Type<unknown>;
  description?: string;
})

Example:

typescript
import { type } from '@onebun/core';

const userResponseSchema = type({
  id: 'string',
  name: 'string',
  email: 'string.email',
});

@Controller('/users')
export class UserController extends BaseController {
  @Get('/:id')
  @ApiResponse(200, {
    schema: userResponseSchema,
    description: 'User found successfully',
  })
  @ApiResponse(404, {
    description: 'User not found',
  })
  async findOne(@Param('id') id: string) {
    // Response will be validated against userResponseSchema
    return { id, name: 'John', email: 'john@example.com' };
  }
}

The schema is enforced, not merely documented: a body that violates it never reaches the client. The request answers HTTP 500 with the default filter's fixed { success: false, error: 'Internal Server Error', code: 500 } — a response that does not match its own declared schema is a server-side contract bug, and the violation detail quotes the offending value, so it goes to the application log rather than to the caller.

Documentation Decorators

Package: @onebun/docs

These decorators add metadata for OpenAPI/Swagger documentation generation.

@ApiTags()

Group endpoints under tags for documentation organization.

typescript
import { ApiTags } from '@onebun/docs';

@ApiTags(...tags: string[])

Can be used on controller class or individual methods:

Example:

typescript
import { Controller, BaseController, Get } from '@onebun/core';
import { ApiTags } from '@onebun/docs';

@ApiTags('Users', 'User Management')
@Controller('/users')
export class UserController extends BaseController {
  // All endpoints tagged with 'Users' and 'User Management'

  @ApiTags('Admin')
  @Get('/admins')
  async getAdmins() {
    return [];
  }
}

@ApiOperation()

Describe an API operation with summary, description, and additional tags.

typescript
import { ApiOperation } from '@onebun/docs';

@ApiOperation(options: {
  summary?: string;
  description?: string;
  tags?: string[];
})

Example:

typescript
import { Controller, BaseController, Get, Param } from '@onebun/core';
import { ApiOperation } from '@onebun/docs';

@Controller('/users')
export class UserController extends BaseController {
  @ApiOperation({
    summary: 'Get user by ID',
    description: 'Returns a single user by their unique identifier. Returns 404 if user not found.',
    tags: ['Users'],
  })
  @Get('/:id')
  async getUser(@Param('id') id: string) {
    return { id, name: 'John' };
  }
}

Combining Documentation Decorators

Use both @onebun/core and @onebun/docs decorators together for complete documentation:

typescript
import { Controller, BaseController, Get, Post, Body, Param, ApiResponse, type } from '@onebun/core';
import { ApiTags, ApiOperation } from '@onebun/docs';

const userSchema = type({
  id: 'string',
  name: 'string',
  email: 'string.email',
});

const createUserSchema = type({
  name: 'string',
  email: 'string.email',
});

type CreateUserBody = typeof createUserSchema.infer;

@ApiTags('Users')
@Controller('/users')
export class UserController extends BaseController {
  @ApiOperation({ summary: 'Get user by ID' })
  @Get('/:id')
  @ApiResponse(200, { schema: userSchema, description: 'User found' })
  @ApiResponse(404, { description: 'User not found' })
  async getUser(@Param('id') id: string) {
    // ...
  }

  @ApiOperation({ summary: 'Create new user', description: 'Creates a new user account' })
  @Post('/')
  @ApiResponse(201, { schema: userSchema, description: 'User created' })
  @ApiResponse(400, { description: 'Invalid input' })
  async createUser(@Body(createUserSchema) body: CreateUserBody) {
    // ...
  }
}

Tracing Decorators

@Span()

Create a trace span for a method (from @onebun/trace).

typescript
@Span(name?: string)

Example:

typescript
import { Span } from '@onebun/trace';

@Service()
export class UserService extends BaseService {
  @Span('user-find-by-id')
  async findById(id: string): Promise<User | null> {
    // This method is automatically traced
    return this.repository.findById(id);
  }

  @Span()  // Default span name is `ClassName.methodName`
  async processUser(user: User): Promise<void> {
    // Span name: "UserService.processUser" — the class prefix is part of it
  }
}

Utility Functions

getControllerMetadata()

Get metadata for a controller class.

typescript
function getControllerMetadata(target: Function): ControllerMetadata | undefined;

interface ControllerMetadata {
  path: string;
  routes: RouteMetadata[];
}

interface RouteMetadata {
  path: string;
  method: HttpMethod;
  handler: string;
  params?: ParamMetadata[];
  middleware?: Function[];
  responseSchemas?: ResponseSchemaMetadata[];
}

getModuleMetadata()

Get metadata for a module class.

typescript
function getModuleMetadata(target: Function): ModuleMetadata | undefined;

interface ModuleMetadata {
  imports?: Function[];
  controllers?: Function[];
  providers?: unknown[];
  exports?: unknown[];
}

getServiceMetadata()

Get metadata for a service class.

typescript
function getServiceMetadata(serviceClass: Function): ServiceMetadata | undefined;

interface ServiceMetadata {
  tag: Context.Tag<unknown, unknown>;
  impl: new () => unknown;
}

getServiceTag()

Get Effect.js Context tag for a service class.

typescript
function getServiceTag<T>(serviceClass: new (...args: unknown[]) => T): Context.Tag<T, T>;

registerDependencies()

Manually register constructor dependencies (fallback method).

typescript
function registerDependencies(target: Function, dependencies: Function[]): void;

Complete Example

typescript
import {
  Module,
  Controller,
  BaseController,
  Service,
  BaseService,
  Get,
  Post,
  Param,
  Body,
  Query,
  Header,
  UseMiddleware,
  BaseMiddleware,
  ApiResponse,
  HttpException,
  type OneBunRequest,
  type OneBunResponse,
  type,
} from '@onebun/core';
import { Span } from '@onebun/trace';

// Validation schemas (in a real app, these live in a separate schema file)
const createUserSchema = type({
  name: 'string',
  email: 'string.email',
});
type CreateUserBody = typeof createUserSchema.infer;

const userSchema = type({
  id: 'string',
  name: 'string',
  email: 'string.email',
});
type User = typeof userSchema.infer;

// Service
@Service()
export class UserService extends BaseService {
  private users = new Map<string, User>();

  @Span('find-all-users')
  async findAll(): Promise<User[]> {
    return Array.from(this.users.values());
  }

  @Span('find-user-by-id')
  async findById(id: string): Promise<User | null> {
    return this.users.get(id) || null;
  }

  async create(data: CreateUserBody): Promise<User> {
    const user = { id: crypto.randomUUID(), ...data };
    this.users.set(user.id, user);
    this.logger.info('User created', { userId: user.id });
    return user;
  }
}

// Middleware
class AuthMiddleware extends BaseMiddleware {
  async use(req: OneBunRequest, next: () => Promise<OneBunResponse>) {
    const token = req.headers.get('Authorization');
    if (!token?.startsWith('Bearer ')) {
      return new Response(JSON.stringify({ success: false, message: 'Unauthorized' }), {
        status: 401,
        headers: { 'Content-Type': 'application/json' },
      });
    }
    return next();
  }
}

// Controller
@Controller('/users')
export class UserController extends BaseController {
  constructor(private userService: UserService) {
    super();
  }

  @Get('/')
  @ApiResponse(200, { schema: userSchema.array() })
  async findAll(
    @Query('limit') limit?: string,
    @Query('offset') offset?: string,
  ) {
    return this.userService.findAll();
  }

  @Get('/:id')
  @ApiResponse(200, { schema: userSchema })
  @ApiResponse(404, { description: 'User not found' })
  async findOne(@Param('id') id: string) {
    const user = await this.userService.findById(id);
    if (!user) throw new HttpException(404, 'User not found');
    return user;
  }

  @Post('/')
  @UseMiddleware(AuthMiddleware)
  @ApiResponse(201, { schema: userSchema })
  async create(
    @Body(createUserSchema) body: CreateUserBody,
    @Header('X-Request-ID') requestId?: string,
  ) {
    this.logger.info('Creating user', { requestId });
    return this.userService.create(body);
  }
}

// Module
@Module({
  controllers: [UserController],
  providers: [UserService],
  exports: [UserService],
})
export class UserModule {}

Released under the MPL-2.0 License.