exjs-controllers

Introduction

Declarative controllers, DTO validation, dependency injection, OpenAPI generation and Express bootstrap for TypeScript APIs.

exjs-controllers

exjs-controllers is a thin, decorator-based framework on top of Express 5 and Zod 4. It lets you describe HTTP APIs as classes with explicit metadata — controllers, parameters, DTOs, authorization, and use cases — and bootstraps the whole application (including OpenAPI 3.1 and Scalar) with a single configureApplication call.

Declarative Controllers

Class-based controllers with `@Controller`, `@JsonController` and HTTP method decorators on top of Express 5.

DTOs With Zod

`BaseSchema` + `@Field` collect Zod definitions, validate request bodies and feed the OpenAPI schema.

Authentication & Authorization

Pluggable principal and authorization checkers with `@Authorized`, `@CurrentUser` and `@CurrentApiKey`.

OpenAPI + Scalar Out of the Box

Auto-generated OpenAPI 3.1 document and Scalar reference UI served by `configureApplication`.

Install

npm install exjs-controllers express zod

Peer dependencies: express ^5, zod ^4. TypeScript projects also need @types/express as a dev dependency. Node.js >=22.

Why exjs-controllers

  • Express, not against it. Routes, middlewares and error handlers are plain Express — controllers register through the same app.get/post/... surface.
  • Schema-first DTOs. BaseSchema + @Field(z....) describe both the runtime validation and the OpenAPI shape.
  • Real DI. Singleton-by-default container with constructor @Inject(token) support, no reflect-metadata required.
  • Auth without lock-in. You provide currentUserChecker and authorizationChecker; the framework only orchestrates them and emits the security blocks in OpenAPI.

Status

Public API current as of version 0.4.0. The library targets the modern (TC39) decorator proposal and continues to accept legacy experimental decorators for older builds.

Get Started

1

Install

Add `exjs-controllers` to your project alongside its peers `express ^5`, `zod ^4` and Node.js `>=20`.

2

Declare

Write `@Controller` classes, declare DTOs that extend `BaseSchema` and annotate fields with `@Field(z....)`.

3

Bootstrap

Call `configureApplication(app, options)` to register controllers, expose OpenAPI and (optionally) mount Scalar.

Public Entry Points

Application

exjs-controllers/http/application

`createApplication()` factory returning the typed `Application` wrapper around Express.

Configuration

exjs-controllers/config/configureApplication

`configureApplication(app, options)` wires controllers, middlewares, auth, logging, OpenAPI and Scalar.

Decorators

exjs-controllers/decorators/*

Controllers, routes, params, DI, authorization, use cases and tracing decorators.

Schemas

exjs-controllers/schemas/BaseSchema

`BaseSchema` base class plus `@Field` for declarative Zod-backed DTOs.

Authentication

exjs-controllers/core/authentication

Auth types, principal kinds, `UnauthorizedError` / `ForbiddenError` and middleware factory.

OpenAPI

exjs-controllers/openapi/generateOpenApiDocument

Programmatic OpenAPI 3.1 document generation from controller metadata.

Observability

exjs-controllers/observability/tracing

`runWithSpan` / `runWithControllerSpan` helpers backed by `@opentelemetry/api`.

Logging

exjs-controllers/logging/*

Pino-based HTTP logger middleware with correlation IDs and async-local request context.

Minimal Server

import { z } from 'zod'
import { createApplication } from 'exjs-controllers/http/application'
import { configureApplication } from 'exjs-controllers/config/configureApplication'
import { Controller } from 'exjs-controllers/decorators/Controller'
import { Get } from 'exjs-controllers/decorators/HttpMethod'
import { Field } from 'exjs-controllers/decorators/Field'
import { BaseSchema } from 'exjs-controllers/schemas/BaseSchema'

class HealthOutput extends BaseSchema {
  @Field(z.string())
  status!: string
}

@Controller('/health')
class HealthController {
  @Get('/', { outputClass: HealthOutput, summary: 'Health check', tags: ['health'] })
  getHealth(): HealthOutput {
    return HealthOutput.create({ status: 'ok' })
  }
}

const app = createApplication()

await configureApplication(app, {
  controllers: [HealthController],
  openapi: {
    documentation: { info: { title: 'Example API', version: '1.0.0' } }
  },
  enableScalar: true
})

app.listen(3000)

What You Get

Dependency Injection

Singleton-by-default container with `@Injectable` and constructor `@Inject(token)` parameters.

Use Cases

`@DefineUseCase` traces and validates `execute(input)` so controllers can dispatch business logic with confidence.

OpenAPI From Metadata

`inputClass` and `outputClass` on routes produce request/response schemas in the generated spec.

Tracing With OpenTelemetry

Controller handlers and `@DefineUseCase` calls run inside spans; `@TraceSpan` wraps any method.

Next Reading

On this page