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.
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-express | Comment |
|---|---|---|
inversify-express-utils | @inversifyjs/http-core + @inversifyjs/http-express | Split into core and adapter packages |
InversifyExpressServer | InversifyExpressHttpAdapter | New 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 + decorator | Removed, use custom implementation |
BaseHttpController | No direct replacement | Use custom implementation with AsyncLocalStorage |
BaseMiddleware | Middleware interface | Simplified middleware interface |
interfaces.Controller | Not required | Controllers don't need to implement an interface |
interfaces.HttpContext | Custom implementation | Use AsyncLocalStorage for request-scoped context |
AuthProvider | Custom middleware | Implement authentication via middleware |
server.setConfig() | Adapter options | Use adapter constructor options |
server.setErrorConfig() | UseErrorFilter decorator | Use error filters |
TYPE.Controller binding | Not required | Controllers are auto-discovered |
cleanUpMetadata() | Not required | Metadata is scoped to container |
getRouteInfo() | Not available | Route introspection not yet implemented |
@withMiddleware() | @ApplyMiddleware() | Renamed decorator |
Installation
First, update your dependencies:
- npm
- pnpm
- yarn
# 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
# Remove old packages
pnpm remove inversify-express-utils
# Install new packages (Express 5)
pnpm add @inversifyjs/http-core @inversifyjs/http-express
# Or for Express 4
pnpm add @inversifyjs/http-core @inversifyjs/http-express-v4
# Remove old packages
yarn remove inversify-express-utils
# Install new packages (Express 5)
yarn add @inversifyjs/http-core @inversifyjs/http-express
# Or for Express 4
yarn add @inversifyjs/http-core @inversifyjs/http-express-v4
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');
});
- Constructor:
InversifyExpressHttpAdaptertakes(container, options)- the Express app is created bybuild() - Body parsing: Use adapter options like
useJsonanduseUrlEncodedinstead ofsetConfig() - Async:
adapter.build()returns a Promise that resolves to the Express application - 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);
}
}
- Decorator naming: Decorators use PascalCase (
@Controller,@Get) instead of camelCase (@controller,@httpGet) - Parameter decorators: Use options objects (
@Params({ name: 'id' })) instead of string arguments (@requestParam('id')) - Status codes: Use
@StatusCodedecorator instead of accessing the response object - Injectable: Controllers should be decorated with
@injectable() - 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 [];
}
}
- Interface: Implement
ExpressMiddlewareinstead of extendingBaseMiddleware - Method name: Use
executeinstead ofhandler - Decorator: Use
@ApplyMiddleware()instead of passing middleware to HTTP method decorators - HttpContext: Not available directly; use
AsyncLocalStoragepattern 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');
}
}
- @CatchError decorator: Error filters must be decorated with
@CatchError(ErrorType)to specify which errors they handle - Throw responses: Error filters throw HTTP response objects instead of returning them
- Controller-level: Apply
@UseErrorFilterat 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-core | Notes |
|---|---|---|
@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 implementation | See 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:
- Decorated with
@injectable() - Bound to the container before calling
adapter.build()
Middleware not executing
Ensure middleware classes:
- Implement the correct interface (
ExpressMiddlewarefor Express) - Are decorated with
@injectable()if they have dependencies - 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
}
}