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.
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
Install express types:
npm install -D typescript @types/express
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;
}
}
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.
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;
}
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);
}
}
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;
}
}
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();
});
}
}
Binding a token to a concrete repository:
export class UserLmdbContainerModule extends ContainerModule {
constructor() {
super((options: ContainerModuleLoadOptions) => {
options
.bind(userPersistenceServiceServiceIdentifier)
.to(LmdbUserRepository)
.inSingletonScope();
});
}
}
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}`);
});
}
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
- Framework homepage: https://inversify.io/framework/
- GitHub monorepo: https://github.com/inversify/monorepo
- Example code in this post: https://github.com/inversify/monorepo/tree/main/packages/framework/http/tools/http-openapi-example
- Community: Discord — https://discord.gg/jXcMagAPnm
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)