@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
- Quick Start
- Framework Architecture
- Request Execution Flow
- Lifecycle Hooks & Application Management
- Testing Strategy
- Best Practices & Common Pitfalls
- Reference Implementation & Examples
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 HonoContextand 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:
PrettyLoggerprovides 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
@Moduledecorator, organizing business logic in three parts:imports,controllers, andproviders. - 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 globalContainerRef. - Provider support:
- Direct class registration (singleton by default).
useClass/useValue/useExisting/useFactory.singleton: falsecan 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 inprovidersunless they have dependencies.
- Container is patched: attempting to resolve unregistered tokens throws a
- 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/@UseFiltersprovide 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 inprovidersif 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:
-
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, }) -
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:
- Global guards → Controller guards → Method guards.
- Interceptors wrap handlers in a stack, supporting pre/post-request processing (logging, caching, response wrapping, etc.).
- Pipes act on input parameters, enabling DTO validation and transformation with Zod engine.
- Exception filters handle errors and return custom Responses.
Middleware System
- Middlewares implement the
HttpMiddlewareinterface and expose an asyncuse(context, next)method. Decorate classes with@Middleware({ path, priority })to attach optional routing metadata. - When no metadata is provided, the framework defaults to
path: '/*'andpriority: 0. Lower priority values run earlier; definitions are sorted before registration so fallback values still participate in ordering. - Metadata from
@MiddlewareandMiddlewareDefinitionobjects 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 moreMiddlewareDefinitionobjects. Each definition must include ahandlerinstance;pathandpriorityremain optional. - Provide middlewares through the dependency-injected
APP_MIDDLEWAREtoken usinguseClass,useExisting,useValue, oruseFactory. Factories may return either aHttpMiddlewareinstance or a fullMiddlewareDefinition.
- Call
- 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 HonoContextinjected by default. - Within any Provider/guard/interceptor, call
HttpContext.get()orHttpContext.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/createZodDtohelper 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
HttpExceptionwith structurederrors.
- 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
PrettyLoggersupports:- Namespace levels (
logger.extend('Router')). - Colored output, symbol/text labels, timestamps.
- Minimum log level (can be auto-downgraded to debug via
DEBUG=true).
- Namespace levels (
- 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,@ApiDocmetadata.
- 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.EventEmitterServiceuses 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/OnModuleDestroyOnApplicationBootstrapBeforeApplicationShutdownOnApplicationShutdown
HonoHttpApplication during init():
- Recursively registers modules and Providers.
- Collects lifecycle participants and instantiates on demand.
- Registers controllers and routes.
- 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
tsyringecontainer 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, useapp.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', notimport 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 withnew(not from container). If your enhancers need DI, useAPP_*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/jsonby default; other types require custom pipes. - Context access:
HttpContextis 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=trueenables 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.