Files
afilmory/be/packages/framework

@afilmory/framework

A lightweight yet feature-complete enterprise framework built on Hono, providing NestJS-like modularity, decorators, and dependency injection while retaining Hono's performance and flexibility.

📚 Table of Contents


Framework Positioning & Features

@afilmory/framework is a server-side framework built around Hono, aimed at providing an enterprise-grade development experience while maintaining performance:

  • Decorator-Driven: Modules, controllers, routes, parameters, and enhancers are all declared using decorators.
  • Dependency Injection: Container based on tsyringe, supporting singleton/factory/provider configurations with strict checking for unregistered dependencies.
  • Request-Scoped Context: Implemented using AsyncLocalStorage, enabling access to Hono Context and custom values at any level.
  • Enhancer System: Guards, Pipes, Interceptors, and Exception Filters work in layers, applicable globally/controller-level/method-level.
  • Type-Safe Validation: Strong typing for requests through Zod schemas + DTO generators.
  • Modern Logging: PrettyLogger provides namespaces, colored output, and level control.
  • OpenAPI Support: Automatically collects decorator metadata to generate OpenAPI 3.1 specification documents.
  • Event-Driven Extension: Built-in Redis-driven event system supporting cross-process pub/sub.

Quick Start

// main.ts
import 'reflect-metadata'
import { serve } from '@hono/node-server'
import { createApplication } from '@afilmory/framework'
import { AppModule } from './app.module'

async function bootstrap() {
  const app = await createApplication(AppModule, {
    globalPrefix: '/api',
  })

  // Register global enhancers (optional) — pass INSTANCES
  // app.useGlobalGuards(new AuthGuard())
  // app.useGlobalPipes(new ValidationPipe())
  // Or use APP_* tokens in module providers (see below)

  const hono = app.getInstance()
  serve({ fetch: hono.fetch, port: 3000 })
}

bootstrap()
// app.module.ts
import { Module, Controller, Get } from '@afilmory/framework'

@Controller('hello')
class HelloController {
  @Get('/')
  sayHi() {
    return { message: 'Hello afilmory!' }
  }
}

@Module({ controllers: [HelloController] })
export class AppModule {}

Framework Architecture

Module System

  • Declare modules using the @Module decorator, organizing business logic in three parts: imports, controllers, and providers.
  • Supports forwardRef(() => OtherModule) to resolve circular dependencies.
  • Modules are initialized only once (singleton) and maintain their own provider collection.
  • Controller instantiation is delayed until route registration; Providers can be instantiated early before lifecycle hooks.
@Module({
  imports: [DatabaseModule],
  controllers: [UserController],
  providers: [UserService],
})
export class UserModule {}

Dependency Injection & Provider Lifecycle

  • Container based on tsyringe; framework creates an independent container instance for the root module at startup and stores it in the global ContainerRef.
  • Provider support:
    • Direct class registration (singleton by default).
    • useClass / useValue / useExisting / useFactory.
    • singleton: false can declare non-singleton providers.
  • Strict DI with smart exceptions:
    • Container is patched: attempting to resolve unregistered tokens throws a ReferenceError, helping quickly locate DI configuration issues.
    • Exception: Classes referenced in enhancer decorators (@UseGuards, @UsePipes, @UseInterceptors, @UseFilters) are auto-registered as singletons on first use, so they don't need to be listed in providers unless they have dependencies.
  • Framework automatically detects Providers implementing lifecycle interfaces and invokes their hooks at appropriate times.
@injectable()
export class UserService implements OnModuleInit, OnModuleDestroy {
  async onModuleInit() {}
  async onModuleDestroy() {}
}

Controllers & Route Mapping

  • @Controller(prefix) specifies the base path; HTTP method decorators (@Get, @Post, etc.) declare routes.
  • Framework reads controller metadata and registers corresponding handlers on the Hono instance.
  • Route parameters are injected via parameter decorators (@Param, @Query, @Body, @Headers, @Req, @ContextParam).
  • Parameters without decorators are automatically inferred as Context, allowing direct access.

Enhancer System: Guards, Pipes, Interceptors, Exception Filters

  • @UseGuards / @UsePipes / @UseInterceptors / @UseFilters provide a unified interface, supporting both class-level and method-level stacking.

  • Auto-registration of decorator classes: Class references in decorators (e.g., @UseGuards(AuthGuard)) are automatically registered as singletons on first use. You don't need to list them in providers if they have no extra dependencies. If they depend on other services, those dependencies must still be registered in a module.

  • Global enhancers can be registered in two ways:

    1. Instance-based via app.useGlobal*() — pass instances only:

      app.useGlobalGuards(new AuthGuard())
      app.useGlobalPipes(new ValidationPipe())
      app.useGlobalInterceptors(new LoggingInterceptor())
      app.useGlobalFilters(new AllExceptionsFilter())
      app.useGlobalMiddlewares({
        handler: new RequestTracingMiddleware(),
        path: ['/api/*', /auth/],
        priority: -10,
      })
      
    2. Module-based via APP_* tokens in providers (NestJS-style):

      import { APP_FILTER, APP_GUARD, APP_INTERCEPTOR, APP_MIDDLEWARE, APP_PIPE } from '@afilmory/framework'
      
      @Module({
        providers: [
          { provide: APP_GUARD, useClass: AuthGuard },
          { provide: APP_PIPE, useValue: preconfiguredPipe },
          { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
          { provide: APP_INTERCEPTOR, useClass: ResponseTransformInterceptor },
          { provide: APP_FILTER, useFactory: (...deps) => new CustomFilter(...deps), inject: [Logger] },
          { provide: APP_MIDDLEWARE, useClass: RequestTracingMiddleware },
        ],
      })
      export class AppModule {}
      

      These are materialized during init() before controllers are bound.

  • Execution order:

    1. Global guards → Controller guards → Method guards.
    2. Interceptors wrap handlers in a stack, supporting pre/post-request processing (logging, caching, response wrapping, etc.).
    3. Pipes act on input parameters, enabling DTO validation and transformation with Zod engine.
    4. Exception filters handle errors and return custom Responses.

Middleware System

  • Middlewares implement the HttpMiddleware interface and expose an async use(context, next) method. Decorate classes with @Middleware({ path, priority }) to attach optional routing metadata.
  • When no metadata is provided, the framework defaults to path: '/*' and priority: 0. Lower priority values run earlier; definitions are sorted before registration so fallback values still participate in ordering.
  • Metadata from @Middleware and MiddlewareDefinition objects is merged—explicit properties win, decorator values fill the gaps. Supported paths include strings, regular expressions, or arrays of both.
  • Registration options:
    • Call app.useGlobalMiddlewares() with one or more MiddlewareDefinition objects. Each definition must include a handler instance; path and priority remain optional.
    • Provide middlewares through the dependency-injected APP_MIDDLEWARE token using useClass, useExisting, useValue, or useFactory. Factories may return either a HttpMiddleware instance or a full MiddlewareDefinition.
  • Middleware handlers participate in lifecycle hooks (OnModuleInit, OnModuleDestroy, etc.) just like other providers.
import type { Context, Next } from 'hono'
import { APP_MIDDLEWARE, Middleware } from '@afilmory/framework'
import { injectable } from 'tsyringe'

// Logger, CacheService, and LegacyMiddleware are regular injectables.

@Middleware({ path: ['/api/*', /reports/], priority: -20 })
@injectable()
class RequestTracingMiddleware {
  constructor(private readonly logger: Logger) {}

  async use(context: Context, next: Next) {
    const start = Date.now()
    await next()
    this.logger.info('request', {
      path: context.req.path,
      durationMs: Date.now() - start,
    })
  }
}

@Module({
  providers: [
    Logger,
    { provide: APP_MIDDLEWARE, useClass: RequestTracingMiddleware },
    {
      provide: APP_MIDDLEWARE,
      useFactory: (cache: CacheService) => ({
        handler: {
          async use(context: Context, next: Next) {
            await next()
            cache.touch(context.req.path)
          },
        },
        path: '/static/*',
        priority: 10,
      }),
      inject: [CacheService],
    },
  ],
})
export class AppModule {}

// Or register imperatively after bootstrap
app.useGlobalMiddlewares({ handler: new LegacyMiddleware(), path: '/legacy' })

Request Context HttpContext

  • Provides request-scoped data container via AsyncLocalStorage, with the current Hono Context injected by default.
  • Within any Provider/guard/interceptor, call HttpContext.get() or HttpContext.getValue('hono') to retrieve context.
  • Supports HttpContext.assign({ userId }) to extend custom values, with type augmentation via module declaration.
declare module '@afilmory/framework' {
  interface HttpContextValues {
    userId?: string
  }
}

HttpContext.assign({ userId: 'u_123' })

Validation & DTOs

  • createZodSchemaDto / createZodDto helper functions: automatically generate DTO classes from Zod Schemas and write metadata.
  • ZodValidationPipe:
    • Transforms to DTO instances by default.
    • Supports strategies like whitelist / forbidUnknownValues / stopAtFirstError.
    • Validation failures throw HttpException with structured errors.
  • Pre-configured pipes can be generated using createZodValidationPipe(options).
const CreateUserSchema = z.object({ email: z.string().email() })
class CreateUserDto extends createZodSchemaDto(CreateUserSchema) {}

@Post('/')
async create(@Body(CreateUserValidationPipe) dto: CreateUserDto) {
	return this.service.create(dto)
}

Logging System

  • PrettyLogger supports:
    • Namespace levels (logger.extend('Router')).
    • Colored output, symbol/text labels, timestamps.
    • Minimum log level (can be auto-downgraded to debug via DEBUG=true).
  • Framework prints debug logs for DI, routing, lifecycle, etc. at critical stages.

OpenAPI Document Generation

  • createOpenApiDocument(rootModule, options) collects module → controller → route hierarchy information.
  • Automatically recognizes:
    • Paths, HTTP methods, OperationIds.
    • Parameter locations (path/query/header) and request body schemas.
    • Zod Schema → JSON Schema conversion (supports optional/nullable, enums, unions, nested structures).
    • Custom @ApiTags, @ApiDoc metadata.
  • Outputs complete OpenAPI 3.1 compliant documentation, including component schemas and module topology (x-modules).

Event System

  • Located in src/events, provides:
    • @OnEvent(event) annotation marks methods as consumers.
    • @EmitEvent(event) automatically publishes events after method success.
    • EventEmitterService uses Redis Pub/Sub for cross-instance broadcasting.
    • EventModule.forRootAsync({ useFactory }) supports async initialization of Redis Client.
  • Runtime scans container for listeners and auto-binds them; releases subscriptions on graceful shutdown.

Request Execution Flow

Inbound Request
	└─> HttpContext.run() establishes request context
			 ├─ Guards (global → controller → method)
			 ├─ Interceptors (forward pre-logic)
			 ├─ Parameter resolution & pipe transformation
			 ├─ Controller handler
			 ├─ Interceptors (reverse post-logic)
			 ├─ Exception filters (if error thrown)
			 └─ Normalized Response output

The framework preserves the original context.res when no explicit return, or automatically wraps objects/strings into Response with marking to avoid duplicate serialization.

Lifecycle Hooks & Application Management

Providers implementing the following interfaces are automatically registered:

  • OnModuleInit / OnModuleDestroy
  • OnApplicationBootstrap
  • BeforeApplicationShutdown
  • OnApplicationShutdown

HonoHttpApplication during init():

  1. Recursively registers modules and Providers.
  2. Collects lifecycle participants and instantiates on demand.
  3. Registers controllers and routes.
  4. Invokes onApplicationBootstrap().

close(signal?) is used for graceful shutdown, triggering in order: before → moduleDestroy → applicationShutdown.

Testing Strategy

Global enhancers:

useGlobal* methods accept instances created with new (not resolved from container). For middlewares, wrap the instance in a MiddlewareDefinition:

const app = await createApplication(AppModule)

// Pass instances created with 'new'
app.useGlobalGuards(new AuthGuard())
app.useGlobalPipes(new ValidationPipe())
app.useGlobalInterceptors(new LoggingInterceptor())
app.useGlobalFilters(new AllExceptionsFilter())
app.useGlobalMiddlewares({ handler: new LegacyMiddleware(), path: '/*' })

If your enhancer needs DI, prefer using APP_* tokens in module providers instead:

import { APP_GUARD, APP_INTERCEPTOR } from '@afilmory/framework'

@Module({
  providers: [
    { provide: APP_GUARD, useClass: AuthGuard },
    { provide: APP_INTERCEPTOR, useClass: LoggingInterceptor },
  ],
})
export class AppModule {}

Testing approaches:

  • Recommended to use vitest (already configured in repository).
  • Unit tests: Leverage tsyringe container to inject mocks and directly resolve Service instances.
    const container = app.getContainer()
    const service = container.resolve(UserService)
    
  • Integration tests: Call createApplication() to construct complete app, use app.getInstance().request() to make requests, testing interceptors, pipes, and filters.
  • Event system: Can be verified by replacing Redis Client (e.g., in-memory mock).

Best Practices & Common Pitfalls

  • Avoid type-only imports: DI resolution requires actual classes. Use import { Service } from './service', not import type { Service }.
  • Decorator auto-registration: Classes in @UseGuards(Guard), @UsePipes(Pipe), etc. are auto-registered as singletons. If they have dependencies, those dependencies must be registered in a module.
  • Global enhancers: app.useGlobal*() accepts instances created with new (not from container). If your enhancers need DI, use APP_* tokens in module providers instead—this is preferred for cleaner organization and proper dependency injection.
  • Provider ordering: If relying on lifecycle hooks, ensure the containing module is correctly imported via imports.
  • Request body JSON validation: Framework validates content-type: application/json by default; other types require custom pipes.
  • Context access: HttpContext is only valid within requests; manual context injection needed in task queues or event callbacks.
  • OpenAPI DTO naming: Schemas use class names by default, customizable via createZodSchemaDto(schema, { name }).
  • Logging & DEBUG: Setting DEBUG=true enables verbose DI/routing logs; production environments should keep default levels.

Reference Implementation & Examples

  • packages/core: Demonstrates framework organization in real services (modules, guards, filters).
  • packages/task-queue, packages/websocket: Illustrate how to write extension modules and cross-package integration.
  • packages/framework/tests: Covers DI, decorator metadata, route execution, and exception handling scenarios—recommended starting point.

For more details, consult AGENTS.md in the repository root (detailed development guide for AI agents) or read the src/ directory source code directly. Feel free to extend modules, enhancers, and tooling based on business needs.