DEV Community

Cover image for Build HTTP APIs with Dependency Injection in TypeScript — Meet the Inversify Framework
notaphplover
notaphplover

Posted on

Build HTTP APIs with Dependency Injection in TypeScript — Meet the Inversify Framework

If you love clean architecture and TypeScript, this guide will show you how to build fast, maintainable HTTP APIs using the Inversify Framework — a decorator-driven, DI-first toolkit on top of InversifyJS.

Sleek hero banner showing an Express app wired to controllers via a DI container, with OpenAPI docs panel on the side

Why another HTTP framework? (Short answer: DI done right)

Inversify brings a familiar pattern from large-scale systems — dependency injection — to your Node.js APIs with first-class TypeScript support. The result is code that’s:

  • Strongly typed: Compile-time safety and IDE autocomplete everywhere.
  • Lightning fast: Minimal overhead with production-ready performance.
  • Decorator-driven: Clean route, middleware, and schema definitions.
  • Framework agnostic: Use Express, Fastify, or others via adapters.
  • Highly extensible: Compose features with container modules and plugins.
  • Battle-tested: Used by thousands, with comprehensive docs.

Explore the docs: https://inversify.io/framework/


What we’ll build in this post

A tiny “Users” API that demonstrates:

  • A DI container wiring services and repositories
  • A REST controller using decorators
  • OpenAPI (v3.1.1) schemas and Swagger UI
  • An Express server bootstrapped via an adapter

The code snippets are taken from the repository example so you can trust the APIs match the real thing.


1) Install required dependencies

Install the core runtime, HTTP adapters, OpenAPI tooling, and supporting libraries:

npm install inversify reflect-metadata \
  @inversifyjs/http-core @inversifyjs/http-express @inversifyjs/http-open-api @inversifyjs/open-api-types \
  express lmdb
Enter fullscreen mode Exit fullscreen mode

Install express types:

npm install -D typescript @types/express
Enter fullscreen mode Exit fullscreen mode

2) Create a controller with decorators

Controllers are plain classes. Routes and bindings are expressed through decorators from @inversifyjs/http-core. Here’s a trimmed version of a real controller:

@Controller('/users')
@OasSummary('User management routes')
export class UserController {
  constructor(
    @inject(UserService) private readonly userService: UserService,
  ) {}

  @OasDescription('Registers a new user in the system')
  @OasOperationId('createUser')
  @OasRequestBody(
    (toSchema: ToSchemaFunction): OpenApi3Dot1RequestBodyObject => ({
      content: { 'application/json': { schema: toSchema(UserCreateQuery) } },
      description: 'User create query',
      required: true,
    }),
  )
  @OasResponse(
    HttpStatusCode.OK,
    (toSchema: ToSchemaFunction): OpenApi3Dot1ResponseObject => ({
      content: { 'application/json': { schema: toSchema(User) } },
      description: 'User created',
    }),
  )
  @OasTag('Users')
  @Post()
  public async register(@Body() userCreateQuery: UserCreateQuery): Promise<UserType> {
    const [user] = await this.userService.create(userCreateQuery);
    return user;
  }
}
Enter fullscreen mode Exit fullscreen mode

What’s happening:

  • @Controller('/users') declares a route base.
  • @Post() maps the method to POST /users.
  • @Body() injects the request body.
  • OpenAPI decorators specify request/response schemas for docs and validation.

An annotated diagram showing decorator-to-route mapping for POST /users


3) Model with OpenAPI schemas

Use decorators to describe your DTOs. This keeps your runtime types and your OpenAPI spec in sync:

import { OasSchema, OasSchemaProperty } from '@inversifyjs/http-open-api';

@OasSchema(undefined, {
  customAttributes: { unevaluatedProperties: false },
  name: 'User',
})
export class User {
  @OasSchemaProperty({ format: 'uuid', type: 'string' })
  public id!: string;

  @OasSchemaProperty({ format: 'email', type: 'string' })
  public email!: string;

  @OasSchemaProperty()
  public name!: string;

  @OasSchemaProperty()
  public surname!: string;
}
Enter fullscreen mode Exit fullscreen mode

4) Business logic with DI (service + repository)

Services and repositories are classes annotated with @injectable() and wired with the container. They can be referenced by class or by token.

@injectable()
export class UserService {
  constructor(
    @inject(userPersistenceServiceServiceIdentifier)
    private readonly userPersistenceService: UserPersistenceService,
  ) {}

  public async create(...users: [UserCreateQueryType]): Promise<[UserType]>;
  public async create(...users: UserCreateQueryType[]): Promise<UserType[]>;
  public async create(...users: UserCreateQueryType[]): Promise<UserType[]> {
    return this.userPersistenceService.insert(...users);
  }
}
Enter fullscreen mode Exit fullscreen mode

A simple repository implemented with LMDB (key-value store) looks like this:

@injectable()
export class LmdbUserRepository implements UserPersistenceService {
  constructor(@inject(lmdbDbServiceIdentifier) private readonly db: RootDatabase) {}

  public async insert(...userCreateQueries: UserCreateQueryDb[]): Promise<UserDb[]> {
    const users: UserDb[] = [];

    await this.db.transaction(async () => {
      for (const q of userCreateQueries) {
        const user: UserDb = { ...q, id: randomUUID() };
        await this.db.put(user.id, user);
        users.push(user);
      }
    });

    return users;
  }
}
Enter fullscreen mode Exit fullscreen mode

5) Wire everything with container modules

Container modules are a clean way to keep bindings organized and composable:

export class UserContainerModule extends ContainerModule {
  constructor() {
    super((options: ContainerModuleLoadOptions) => {
      options.bind(UserController).toSelf().inSingletonScope();
      options.bind(UserService).toSelf().inSingletonScope();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

Binding a token to a concrete repository:

export class UserLmdbContainerModule extends ContainerModule {
  constructor() {
    super((options: ContainerModuleLoadOptions) => {
      options
        .bind(userPersistenceServiceServiceIdentifier)
        .to(LmdbUserRepository)
        .inSingletonScope();
    });
  }
}
Enter fullscreen mode Exit fullscreen mode

A simple box-and-arrow diagram showing Container → Controller → Service → Repository with mermaid graph


6) Bootstrap an Express server (and docs) in a few lines

Use the Express adapter and the Swagger UI provider from the framework packages:

const PORT = 3000;

export async function bootstrap(): Promise<void> {
  const container = new Container();

  await container.load(
    new LmdbContainerModule(),
    new UserContainerModule(),
    new UserLmdbContainerModule(),
  );

  const adapter = new InversifyExpressHttpAdapter(container, {
    logger: true,
    useCookies: false,
    useJson: true,
  });

  const swaggerProvider = new SwaggerUiProvider({
    api: {
      openApiObject: {
        info: { title: 'My awesome API', version: '1.0.0' },
        openapi: '3.1.1',
      },
      path: '/docs',
    },
    ui: { title: 'My awesome API docs' },
  });

  swaggerProvider.provide(container);

  const app: express.Application = await adapter.build();
  app.listen(PORT, () => {
    console.log(`Server started on http://localhost:${PORT}`);
  });
}
Enter fullscreen mode Exit fullscreen mode

That’s it — your routes are discovered via the container, and your OpenAPI docs are available at /docs.


Where does this shine?

  • Large codebases: Constructor injection makes testing and refactoring painless.
  • Cross-cutting concerns: Middleware, logging, and auth are cleanly composed.
  • Multiple runtimes: Swap adapters (Express, Fastify, …) without rewriting controllers.
  • Schema-first teams: Generate and serve OpenAPI (v3.1.1) docs from your code.

Learn more and go deeper


Final thoughts

Inversify makes HTTP APIs feel like well-structured applications, not scripts. If you value clean boundaries, testability, and TypeScript ergonomics, it’s a perfect fit. Start small with a controller and a service — the DI container will grow with you.

Top comments (0)