Skip to content

innei-template/Hono-Enterprise-Template

Repository files navigation

Hono Enterprise Template

A NestJS‑inspired, Hono‑powered enterprise template for building modular, type‑safe HTTP services. The core framework package ships with dependency injection, decorators, guards, pipes, interceptors, exception filters, request‑scoped context, and an extensible pretty logger. The framework tests achieve 100% coverage and the sample app demonstrates all enhancement paths end‑to‑end.

✨ Features

  • Hono application layer: Hono performance with opinionated structure and decorators.
  • Modular architecture + DI: tsyringe-based container, constructor injection, module imports/exports.
  • Request context: HttpContext built on AsyncLocalStorage to safely access the current Context anywhere.
  • Composables (enhancers): Guards, Pipes, Interceptors, and Exception Filters with a declarative API.
  • Zod validation pipe: metadata-driven DTO validation via createZodValidationPipe({ ... }) and @ZodSchema decorators.
  • Pretty logger: Namespaced, colorized output with CI-safe text labels and hierarchical extend().
  • Task queue decorators: Register background job handlers with @TaskProcessor() and let the queue wire itself up.
  • OpenAPI explorer: Generate OpenAPI 3.1 docs from decorators and serve them through Scalar.
  • First-class testing: Framework Vitest suite with 100% coverage; demo app covers all enhancer paths.
  • Infrastructure providers via DI: Postgres (Drizzle) and Redis (ioredis) wired as modules.

πŸ“ Monorepo Layout

Path Description
apps/core Demo application showcasing modules, controllers, and all enhancers; usable as a starter
packages/framework Core framework: HonoHttpApplication, decorators, HTTP context, logger, Zod pipe, etc.
packages/framework/tests Vitest suite for the framework with coverage and lifecycle tests
packages/db Drizzle schema & types plus migrations configuration
packages/env Runtime env validation powered by @t3-oss/env-core
packages/redis Redis client factory (ioredis) and strong types
packages/websocket Redis-backed WebSocket gateway with pub/sub broker, heartbeat management, and logging

βœ… Requirements

  • Node.js 18+ (uses AsyncLocalStorage and modern ESM tooling)
  • pnpm 10+
  • TypeScript 5.9

πŸš€ Quickstart

# install dependencies
pnpm install

# run framework tests (with coverage)
pnpm -C packages/framework test

# run demo app tests
pnpm -C apps/core test

# start the demo app (vite-node)
pnpm -C apps/core dev

# or run the in-process demo runner
pnpm -C apps/web dev

Coverage reports are generated at packages/framework/coverage.

πŸ”§ Environment

Create a .env file at the repo root with at least:

DATABASE_URL=postgres://user:pass@localhost:5432/db
REDIS_URL=redis://localhost:6379

# Optional WebSocket gateway configuration
# WEBSOCKET_ENABLED=true
# WEBSOCKET_PORT=8081
# WEBSOCKET_PATH=/ws
# WEBSOCKET_HEARTBEAT_INTERVAL_MS=30000

# Optional Postgres pool tuning
PG_POOL_MAX=10
PG_IDLE_TIMEOUT=30000
PG_CONN_TIMEOUT=5000

🧱 Architecture & Runtime Model

1) Modules and Controllers

import { Controller, Get, Query, UseGuards, Module } from '@hono-template/framework'

@Controller('demo')
export class DemoController {
  constructor(private readonly service: DemoService) {}

  @Get('/hello')
  @UseGuards(ApiKeyGuard)
  async greet(@Query('name') name: string) {
    return this.service.greet(name)
  }
}

@Module({
  controllers: [DemoController],
  providers: [DemoService, ApiKeyGuard],
})
export class DemoModule {}

Bootstrapping with createApplication(RootModule, options) performs:

  1. Recursive module registration via imports.
  2. DI registration of providers and controllers using tsyringe.
  3. Route discovery from class/method decorators and mapping to Hono.
  4. Per-request pipeline: Guards β†’ Pipes (global/method/parameter) β†’ Interceptors β†’ Controller β†’ Filters.

1.1) Provider Lifecycle

Providers and controllers may implement lifecycle interfaces inspired by NestJS:

  • OnModuleInit β†’ onModuleInit() after a module and its imports finish registering.
  • OnApplicationBootstrap β†’ onApplicationBootstrap() after the app finishes initialization.
  • BeforeApplicationShutdown β†’ beforeApplicationShutdown(signal?) prior to shutdown.
  • OnModuleDestroy β†’ onModuleDestroy() during teardown.
  • OnApplicationShutdown β†’ onApplicationShutdown(signal?) as the final shutdown step.

Call await app.close('SIGTERM') on the HonoHttpApplication instance to trigger shutdown hooks.

2) Enhancers (Guards, Pipes, Interceptors, Filters)

  • @UseGuards(...guards): CanActivate.canActivate(ctx) returning boolean | Promise<boolean>. false throws ForbiddenException.
  • @UsePipes(...pipes) and parameter-level pipes (e.g., @Param('id', ParseIntPipe)): merged globally and per-method.
  • @UseInterceptors(...interceptors): interceptor.intercept(context, next) chaining.
  • @UseFilters(...filters): handle and customize error responses; unhandled errors return a 500 JSON payload.

Zod validation is provided by registering DTO classes with createZodSchemaDto(...) (or the lower-level @ZodSchema(...)) and enabling a global createZodValidationPipe({ ... }). See packages/framework/tests/application.spec.ts for full examples.

2.5) Infrastructure Modules: Database (Postgres) and Redis

Both the database and Redis are registered as DI-driven modules in the demo app.

  • Database lives under apps/core/src/database and exposes a DbAccessor that returns a request-aware Drizzle instance.
  • Redis lives under apps/core/src/redis and exposes a RedisAccessor that returns a singleton ioredis client.

Ensure DatabaseModule and RedisModule are imported by your root module (already wired in the demo):

import { Module } from '@hono-template/framework'
import { DatabaseModule } from '../database/module'
import { RedisModule } from '../redis/module'
import { AppModule } from './app/app.module'

@Module({
  imports: [DatabaseModule, RedisModule, AppModule],
})
export class AppModules {}

Using Redis from a service via DI:

import { injectable } from 'tsyringe'
import { RedisAccessor } from '../redis/providers'

@injectable()
export class CacheService {
  constructor(private readonly redis: RedisAccessor) {}

  async setGreeting(key: string, name: string): Promise<void> {
    await this.redis.get().set(key, name, 'EX', 60)
  }

  async getGreeting(key: string): Promise<string | null> {
    return await this.redis.get().get(key)
  }
}

2.6) WebSocket Gateway

The @hono-template/websocket package provides a Redis-backed WebSocket gateway with channel subscriptions, Redis pub/sub fan-out, and automatic heartbeat/ping management. The demo app exposes it through WebSocketDemoModule (disabled by default). The /api/websocket/info route reports status, and /api/websocket/channels/:channel/publish publishes payloads to connected clients.

2.7) Task Queue with Decorators

The @hono-template/task-queue package ships with a decorator-driven registration model so workers only need to annotate their handler methods:

import { injectable } from 'tsyringe'
import { OnModuleDestroy, OnModuleInit } from '@hono-template/framework'
import { RedisQueueDriver, TaskContext, TaskProcessor, TaskQueue, TaskQueueManager } from '@hono-template/task-queue'

@injectable()
export class NotificationQueue implements OnModuleInit, OnModuleDestroy {
  public queue!: TaskQueue

  constructor(
    private readonly manager: TaskQueueManager,
    private readonly redis: RedisAccessor,
  ) {}

  async onModuleInit(): Promise<void> {
    const driver = new RedisQueueDriver({
      redis: this.redis.get(),
      queueName: 'core:notifications',
      visibilityTimeoutMs: 45_000,
    })

    this.queue = this.manager.createQueue('notifications', {
      driver,
      start: false,
      middlewares: [
        async (ctx, next) => {
          ctx.logger.debug('start', { taskId: ctx.taskId })
          await next()
        },
      ],
    })

    await this.queue.start({ pollIntervalMs: 250 })
  }

  @TaskProcessor('send-notification', {
    options: {
      maxAttempts: 5,
      retryableFilter: () => true,
      backoffStrategy: (attempt) => Math.min(30_000, 2 ** attempt * 250),
    },
  })
  async sendNotification(payload: NotificationPayload, context: TaskContext<NotificationPayload>): Promise<void> {
    // business logic here
    context.logger.info('Delivered notification', { taskId: context.taskId })
  }

  async onModuleDestroy(): Promise<void> {
    await this.queue?.shutdown()
  }
}

@TaskProcessor() delays registration until onModuleInit finishes so that the queue instance is ready, supports alternate queue property names, and accepts per-handler options (or an options factory). Any service can inject the queue to enqueue work:

@injectable()
export class NotificationService {
  constructor(private readonly worker: NotificationQueue) {}

  async enqueue(payload: NotificationPayload) {
    return await this.worker.queue.enqueue({ name: 'send-notification', payload })
  }
}

2.8) OpenAPI & Interactive Docs

The framework can build an OpenAPI 3.1 document directly from module and controller decorators and expose it alongside a Scalar-powered UI.

import type { Hono } from 'hono'
import { ApiDoc, ApiTags, createOpenApiDocument } from '@hono-template/framework'

import { AppModules } from './modules/index.module'

function registerDocs(app: Hono, prefix = '/api') {
  const document = createOpenApiDocument(AppModules, {
    title: 'Core Service API',
    version: '1.0.0',
    description: 'Decorator-generated OpenAPI spec',
    globalPrefix: prefix,
    servers: [{ url: prefix }],
  })

  const specPath = `${prefix}/openapi.json`
  const docsPath = `${prefix}/docs`

  app.get(specPath, (ctx) => ctx.json(document))
  app.get(docsPath, (ctx) => ctx.html(renderScalarHtml(specPath)))
}

createOpenApiDocument() groups operations by module and controller, providing consistent tags for consumers, while the Scalar embed above mirrors the recommended CDN integration.

Decorate controllers or individual handlers with @ApiTags() to introduce domain-specific groupings, and use @ApiDoc({ summary, tags, deprecated, ... }) to fine-tune operation metadata without leaving your code.

3) Result Handling

Handlers may return Response, string, ArrayBuffer, ArrayBufferView, ReadableStream, or plain objects. Non-Response values are normalized to a proper HTTP response. undefined or returning context.res preserves the current response.

4) Logger

import { createLogger } from '@hono-template/framework'

const logger = createLogger('App')
logger.info('Service started')
logger.warn('Auth failed', { userId })

const scoped = logger.extend('Module')
scoped.debug('Loaded')

Logger options include custom writer, color strategy, clock, per-level colors, and CI-safe text labels. The framework uses namespaces Framework, Framework:DI, and Framework:Router internally.

5) Request Context

HttpContext.run(context, fn) establishes a request scope backed by AsyncLocalStorage. The store is a typed object that always includes the active Hono Context as store.hono and can be extended via module augmentation. Use HttpContext.get()/HttpContext.getValue('hono') inside guards, interceptors, or services, and HttpContext.assign()/setValue() to attach custom request metadata.

πŸ§ͺ Testing & Quality

  • Framework tests: pnpm -C packages/framework test (coverage threshold 100%).
  • Demo app tests: pnpm -C apps/core test.
  • Type checking: use TypeScript 5.9; optionally run pnpm tsc --noEmit at the repo root.

🧩 Developer Guide

Bootstrapping an App

import 'reflect-metadata'
import { serve } from '@hono/node-server'
import { createApplication, createZodValidationPipe } from '@hono-template/framework'
import { AppModule } from './app.module'

const ValidationPipe = createZodValidationPipe({
  transform: true,
  whitelist: true,
  errorHttpStatusCode: 422,
  forbidUnknownValues: true,
})

const app = await createApplication(AppModule, { globalPrefix: '/api' })
app.useGlobalPipes(ValidationPipe)
app.useGlobalFilters(AllExceptionsFilter)
app.useGlobalInterceptors(LoggingInterceptor)

const hono = app.getInstance()

serve({ fetch: hono.fetch, port: 3000 })

Dependency Injection & Types

Use tsyringe decorators for providers and constructor injection. When running through transpilers that strip design metadata (e.g. esbuild), add a Reflect.metadata shim so runtime DI still sees parameter types:

import 'reflect-metadata'
import { injectable } from 'tsyringe'
import { Controller, Get } from '@hono-template/framework'

@injectable()
class AppService {
  getHello(echo?: string | null) {
    return {
      message: 'Hello',
      timestamp: new Date().toISOString(),
      echo: echo ?? undefined,
    }
  }
}

@Controller('app')
@injectable()
@Reflect.metadata('design:paramtypes', [AppService])
class AppController {
  constructor(private readonly service: AppService) {}

  @Get('/')
  getRoot() {
    return this.service.getHello()
  }
}

Parameter Decorators

@Body, @Query, @Param, @Headers, @Req, @ContextParam extract values and optionally run per-parameter pipes.

Exceptions

Throw HttpException or built-ins like BadRequestException, ForbiddenException, NotFoundException. Custom filters may translate errors into consistent API responses.

Validation with Zod

import { z } from 'zod'
import { Body, Controller, Post, createZodSchemaDto } from '@hono-template/framework'

const CreateMessageSchema = z.object({
  message: z.string().min(1),
  tags: z.array(z.string()).default([]),
})

class CreateMessageDto extends createZodSchemaDto(CreateMessageSchema) {}

@Controller('messages')
class MessagesController {
  @Post('/:id')
  create(@Body() body: CreateMessageDto) {
    return { status: 'queued', ...body }
  }
}

Alternative: call createZodDto(CreateMessageSchema) to obtain a ready-to-use class without extending.

πŸ“œ Scripts

In apps/core/package.json:

  • dev: start the demo server with vite-node.
  • demo: run an in-process demo exercising routes and enhancers.
  • test: run tests for the demo app.

πŸ”— References & Inspiration

  • NestJS β€” decorator-driven, layered application architecture.
  • Hono β€” small, fast web framework.
  • tsyringe β€” lightweight dependency injection container.
  • Zod β€” type-safe schema validation.

Customize the framework under packages/framework/src and use apps/core as a reference implementation for modules, controllers, and enhancers. Consider extending with enterprise capabilities (configuration, CQRS, event bus, etc.) as your project evolves.

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published