Skip to main content

Migrating from inversify-express-utils

This guide helps you migrate from inversify-express-utils to @inversifyjs/http-express (or @inversifyjs/http-express-v4 for Express 4). The new HTTP packages are part of the InversifyJS monorepo and offer improved performance, better TypeScript support, and compatibility with InversifyJS v7.

Why migrate?

inversify-express-utils is deprecated and does not support InversifyJS v7. The new @inversifyjs/http-* packages provide:

  • Full compatibility with InversifyJS v7
  • Support for multiple HTTP frameworks (Express, Fastify, Hono, uWebSockets.js)
  • Improved performance and smaller bundle size
  • Better TypeScript type inference
  • Modern decorator-based API with guards, interceptors, pipes, and error filters

Overview of Changes

The following table summarizes the key changes from inversify-express-utils to @inversifyjs/http-express:

inversify-express-utils@inversifyjs/http-expressComment
inversify-express-utils@inversifyjs/http-core + @inversifyjs/http-expressSplit into core and adapter packages
InversifyExpressServerInversifyExpressHttpAdapterNew adapter-based architecture
@controller("/path")@Controller("/path")Renamed decorator (PascalCase)
@httpGet, @httpPost, etc.@Get, @Post, etc.Renamed decorators (shorter names)
@request()@Request()Renamed decorator (PascalCase)
@response()@Response()Renamed decorator (PascalCase)
@requestParam("id")@Params({ name: "id" })Renamed with options object
@queryParam("search")@Query({ name: "search" })Renamed with options object
@requestBody()@Body()Renamed decorator
@requestHeaders("name")@Headers({ name: "name" })Renamed with options object
@cookies("name")@Cookies({ name: "name" })Renamed with options object
@next()@Next()Renamed decorator (PascalCase)
@principal()Custom middleware + decoratorRemoved, use custom implementation
BaseHttpControllerNo direct replacementUse custom implementation with AsyncLocalStorage
BaseMiddlewareMiddleware interfaceSimplified middleware interface
interfaces.ControllerNot requiredControllers don't need to implement an interface
interfaces.HttpContextCustom implementationUse AsyncLocalStorage for request-scoped context
AuthProviderCustom middlewareImplement authentication via middleware
server.setConfig()Adapter optionsUse adapter constructor options
server.setErrorConfig()UseErrorFilter decoratorUse error filters
TYPE.Controller bindingNot requiredControllers are auto-discovered
cleanUpMetadata()Not requiredMetadata is scoped to container
getRouteInfo()Not availableRoute introspection not yet implemented
@withMiddleware()@ApplyMiddleware()Renamed decorator

Installation

First, update your dependencies:

# Remove old packages
npm uninstall inversify-express-utils

# Install new packages (Express 5)
npm install @inversifyjs/http-core @inversifyjs/http-express

# Or for Express 4
npm install @inversifyjs/http-core @inversifyjs/http-express-v4
InversifyJS v7

Make sure you're using InversifyJS v7. If you're migrating from v6, see the InversifyJS v7 migration guide first.

Server Setup

Before (inversify-express-utils)

import * as bodyParser from 'body-parser';
import { Container } from 'inversify';
import { InversifyExpressServer } from 'inversify-express-utils';

import './controllers/foo_controller';

const container = new Container();
container.bind<FooService>('FooService').to(FooService);

const server = new InversifyExpressServer(container);

server.setConfig((app) => {
app.use(bodyParser.urlencoded({ extended: true }));
app.use(bodyParser.json());
});

server.setErrorConfig((app) => {
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send('Something broke!');
});
});

const app = server.build();
app.listen(3000);

After (@inversifyjs/http-express)

const container: Container = new Container();

// Bind services
container.bind('FooService').to(FooService);

// Bind controllers
container.bind(FooController).toSelf();

// Create the adapter with options
const adapter: InversifyExpressHttpAdapter = new InversifyExpressHttpAdapter(
container,
{
logger: true,
useJson: true,
useUrlEncoded: true,
},
);

// Build the Express application
const app: express.Application = await adapter.build();

// Start the server
app.listen(3000, () => {
console.log('Server listening on port 3000');
});
Key differences
  1. Constructor: InversifyExpressHttpAdapter takes (container, options) - the Express app is created by build()
  2. Body parsing: Use adapter options like useJson and useUrlEncoded instead of setConfig()
  3. Async: adapter.build() returns a Promise that resolves to the Express application
  4. Controller binding: Controllers must be explicitly bound to the container

Controllers

Before (inversify-express-utils)

import { Request, Response } from 'express';
import { inject } from 'inversify';
import {
controller,
httpGet,
httpPost,
httpDelete,
request,
response,
requestParam,
queryParam,
requestBody,
} from 'inversify-express-utils';

@controller('/foo')
export class FooController {
constructor(@inject('FooService') private fooService: FooService) {}

@httpGet('/')
private list(
@queryParam('start') start: number,
@queryParam('count') count: number,
): string[] {
return this.fooService.findAll(count);
}

@httpPost('/')
private async create(
@requestBody() body: CreateFooDto,
@response() res: Response,
): Promise<void> {
await this.fooService.create(body);
res.sendStatus(201);
}

@httpDelete('/:id')
private async delete(
@requestParam('id') id: string,
): Promise<void> {
await this.fooService.delete(id);
}
}

After (@inversifyjs/http-express)

@injectable()
@Controller('/foo')
export class FooController {
constructor(@inject('FooService') private readonly fooService: FooService) {}

@Get()
public list(
@Query({ name: 'start' }) _start: number,
@Query({ name: 'count' }) count: number,
): string[] {
return this.fooService.findAll(count);
}

@StatusCode(HttpStatusCode.CREATED)
@Post()
public async create(@Body() body: CreateFooDto): Promise<void> {
await this.fooService.create(body);
}

@Delete('/:id')
public async delete(@Params({ name: 'id' }) id: string): Promise<void> {
await this.fooService.delete(id);
}
}
Key differences
  1. Decorator naming: Decorators use PascalCase (@Controller, @Get) instead of camelCase (@controller, @httpGet)
  2. Parameter decorators: Use options objects (@Params({ name: 'id' })) instead of string arguments (@requestParam('id'))
  3. Status codes: Use @StatusCode decorator instead of accessing the response object
  4. Injectable: Controllers should be decorated with @injectable()
  5. Return values: Return values are automatically serialized as JSON

Middleware

Before (inversify-express-utils)

import { injectable, inject } from 'inversify';
import { BaseMiddleware } from 'inversify-express-utils';
import { Request, Response, NextFunction } from 'express';

@injectable()
class LoggerMiddleware extends BaseMiddleware {
@inject('Logger') private logger: Logger;

public handler(req: Request, res: Response, next: NextFunction): void {
this.logger.info(`${req.method} ${req.url}`);
next();
}
}

// Using middleware on a route
@controller('/users')
class UserController {
@httpGet('/', TYPES.LoggerMiddleware)
public getUsers(): User[] {
return [];
}
}

After (@inversifyjs/http-express)

@injectable()
export class LoggerMiddleware implements ExpressMiddleware {
constructor(@inject('Logger') private readonly logger: Logger) {}

public execute(req: Request, _res: Response, next: NextFunction): void {
this.logger.info(`${req.method} ${req.url}`);
next();
}
}

Using middleware on a route:

@injectable()
@Controller('/users')
export class UserController {
@ApplyMiddleware(LoggerMiddleware)
@Get()
public getUsers(): User[] {
return [];
}
}
Key differences
  1. Interface: Implement ExpressMiddleware instead of extending BaseMiddleware
  2. Method name: Use execute instead of handler
  3. Decorator: Use @ApplyMiddleware() instead of passing middleware to HTTP method decorators
  4. HttpContext: Not available directly; use AsyncLocalStorage pattern if needed (see below)

Request-scoped Middleware (BaseMiddleware.bind)

In inversify-express-utils, BaseMiddleware provided a bind() method to create request-scoped bindings. In the new packages, attach data to the request object.

Before

@injectable()
class TracingMiddleware extends BaseMiddleware {
public handler(req: Request, res: Response, next: NextFunction): void {
this.bind<string>(TYPES.TraceId).toConstantValue(req.header('X-Trace-Id') ?? '');
next();
}
}

After

Extend the Express Request type and attach data directly:

export class TracingMiddleware implements ExpressMiddleware {
public execute(req: Request, _res: Response, next: NextFunction): void {
req.traceId = req.header('X-Trace-Id') ?? '';
next();
}
}

Access in controller:

@injectable()
@Controller('/example')
export class TracingController {
@Get()
public example(@Request() req: ExpressRequest): string {
return `Trace ID: ${req.traceId ?? 'unknown'}`;
}
}

BaseHttpController and HttpContext

inversify-express-utils provided BaseHttpController with access to httpContext containing the request, response, and user principal. The new packages don't include this pattern, but you can implement it using Node.js AsyncLocalStorage.

Implementing HttpContext with AsyncLocalStorage

First, create a middleware to set up the context:

export interface HttpContext {
request: Request;
response: Response;
user?: unknown;
}

export const httpContextStorage: AsyncLocalStorage<HttpContext> =
new AsyncLocalStorage<HttpContext>();

@injectable()
export class HttpContextMiddleware implements ExpressMiddleware {
public execute(req: Request, res: Response, next: NextFunction): void {
httpContextStorage.run({ request: req, response: res }, () => {
next();
});
}
}

Then create a base controller with context access:

@injectable()
export abstract class BaseHttpController {
protected get httpContext(): HttpContext {
const context: HttpContext | undefined = httpContextStorage.getStore();
if (context === undefined) {
throw new Error('HttpContext not available outside request scope');
}
return context;
}
}

@injectable()
@Controller('/users')
export class UserController extends BaseHttpController {
@Get('/me')
public getCurrentUser(): unknown {
return this.httpContext.user;
}
}

Register HttpContextMiddleware as a global middleware to make context available in all controllers:

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

adapter.applyGlobalMiddleware(HttpContextMiddleware);

const app = await adapter.build();

AuthProvider and Principal

inversify-express-utils had a built-in authentication system with AuthProvider and Principal. The new packages delegate authentication to external solutions or custom implementations.

Before (inversify-express-utils)

import { injectable, inject } from 'inversify';
import { interfaces } from 'inversify-express-utils';

@injectable()
class CustomAuthProvider implements interfaces.AuthProvider {
@inject('AuthService') private authService: AuthService;

public async getUser(
req: Request,
res: Response,
next: NextFunction,
): Promise<interfaces.Principal> {
const token = req.headers['x-auth-token'];
const user = await this.authService.getUser(token);
return new Principal(user);
}
}

const server = new InversifyExpressServer(
container, null, null, null, CustomAuthProvider
);

After (@inversifyjs/http-express)

Use a middleware combined with the AsyncLocalStorage pattern shown above:

@injectable()
export class AuthMiddleware implements ExpressMiddleware {
constructor(
@inject('AuthService') private readonly authService: AuthService,
) {}

public async execute(
req: Request,
_res: Response,
next: NextFunction,
): Promise<void> {
const context: HttpContext | undefined = httpContextStorage.getStore();
if (context !== undefined) {
const token: string | undefined = req.headers['x-auth-token'] as
| string
| undefined;
if (token !== undefined) {
context.user = await this.authService.getUser(token);
}
}
next();
}
}

Alternatively, consider using Better Auth integration for a complete authentication solution.

Error Handling

Before (inversify-express-utils)

const server = new InversifyExpressServer(container);

server.setErrorConfig((app) => {
app.use((err: Error, req: Request, res: Response, next: NextFunction) => {
console.error(err.stack);
res.status(500).json({ error: err.message });
});
});

After (@inversifyjs/http-express)

Use the @CatchError and @UseErrorFilter decorators with custom error filters:

@CatchError(Error)
export class GlobalErrorFilter implements ErrorFilter<Error> {
public catch(error: Error): void {
console.error(error);
throw new InternalServerErrorHttpResponse(
{ error: error.message },
'Internal server error',
{ cause: error },
);
}
}

@injectable()
@UseErrorFilter(GlobalErrorFilter)
@Controller('/example')
export class ExampleController {
@Get()
public example(): string {
throw new Error('Something went wrong');
}
}
Key differences
  1. @CatchError decorator: Error filters must be decorated with @CatchError(ErrorType) to specify which errors they handle
  2. Throw responses: Error filters throw HTTP response objects instead of returning them
  3. Controller-level: Apply @UseErrorFilter at the controller or method level

See Error Filters for more details.

Controller Binding

Before (inversify-express-utils)

// Controllers were auto-discovered via side-effect imports
import './controllers/user_controller';

// Or manually bound with TYPE.Controller
container.bind<interfaces.Controller>(TYPE.Controller)
.to(UserController)
.whenTargetNamed('UserController');

After (@inversifyjs/http-express)

import { UserController } from './controllers/user_controller';

// Bind controllers to the container
container.bind(UserController).toSelf();

HTTP Method Decorators

inversify-express-utils@inversifyjs/http-core
@httpGet(path)@Get(path)
@httpPost(path)@Post(path)
@httpPut(path)@Put(path)
@httpPatch(path)@Patch(path)
@httpDelete(path)@Delete(path)
@httpHead(path)@Head(path)
@httpOptions(path)@Options(path)
@All(path)@All(path)
@httpMethod(method, path)Use specific decorator

Parameter Decorators

inversify-express-utils@inversifyjs/http-coreNotes
@request()@Request()PascalCase
@response()@Response()PascalCase
@requestParam(name)@Params({ name })Options object
@requestParam()@Params()Get all params
@queryParam(name)@Query({ name })Options object
@queryParam()@Query()Get all query params
@requestBody()@Body()Renamed
@requestHeaders(name)@Headers({ name })Options object
@cookies(name)@Cookies({ name })Options object
@next()@Next()PascalCase
@principal()Custom implementationSee AuthProvider section

New Features

The new packages include several features not available in inversify-express-utils:

  • Guards: Authorization checks that run before handlers
  • Interceptors: Pre/post processing of requests and responses
  • Pipes: Parameter transformation and validation
  • Error Filters: Structured error handling
  • Multiple HTTP adapters: Support for Express, Fastify, Hono, and uWebSockets.js
  • OpenAPI integration: Automatic API documentation generation

Troubleshooting

"Controller not found" errors

Make sure your controllers are:

  1. Decorated with @injectable()
  2. Bound to the container before calling adapter.build()

Middleware not executing

Ensure middleware classes:

  1. Implement the correct interface (ExpressMiddleware for Express)
  2. Are decorated with @injectable() if they have dependencies
  3. Call next() to continue the request pipeline

Body parsing not working

Use the adapter options to configure body parsing:

const adapter = new InversifyExpressHttpAdapter(container, {
useJson: true,
useUrlEncoded: true,
});

TypeScript decorator errors

Ensure your tsconfig.json has:

{
"compilerOptions": {
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}