Skip to main content

Controller

Controllers are classes annotated with the @Controller() decorator. They group request handlers for a feature area. A controller defines a base path and one or more route handlers using method decorators.

Controller and injectable decorators

You don't need to add @injectable(). The @Controller() decorator already applies it for you.

Basic example

A minimal controller with one route:

@Controller('/messages')
export class MessagesController {
@Get('/hello')
public async sayHello(): Promise<Message> {
return { content: 'world' };
}
}

Registering a controller

Register controllers in your container. Bind your controller class and choose a scope.

const container: Container = new Container();
// Register the controller so the adapter can discover it
container.bind(MessagesController).toSelf().inSingletonScope();

When you call build() on your server adapter, it builds routes from controller metadata at runtime.

Implementation approaches

There are two complementary ways to write controller routes in Inversify HTTP. You can keep your handlers framework-agnostic and let Inversify send the response for you, or you can opt into native adapter types and take full control of the underlying framework's response flow.

With the framework-agnostic approach, your controller methods return values and Inversify converts them into HTTP responses. The adapter decides how to shape the response based on the value: strings are sent as plain text, objects (including undefined) are sent as JSON, Node streams are piped as streaming responses, and primitive values like numbers or booleans are stringified as text. The status code is 200 by default, but you can override it per route with the @StatusCode() decorator or by returning a typed HttpResponse (for example, new CreatedHttpResponse(body) or throwing an ErrorHttpResponse). This keeps controllers portable across adapters and focuses them on business logic rather than framework details.

@Controller('/users')
export class NonNativeUsersController {
// Return plain value - Inversify converts to JSON response
@Get()
@SetHeader('X-Custom-Header', 'CustomValue')
public async getUsers(): Promise<User[]> {
return [
{ email: 'john@example.com', id: 1, name: 'John Doe' },
{ email: 'jane@example.com', id: 2, name: 'Jane Smith' },
];
}

// Return specific HttpResponse for custom status codes
@Post()
public async createUser(
@Body() userData: CreateUserRequest,
): Promise<CreatedHttpResponse> {
const newUser: User = {
email: userData.email,
id: Math.random(),
name: userData.name,
};

return new CreatedHttpResponse(newUser);
}

// Throw ErrorHttpResponse for error conditions
@Get('/not-found')
public async getUserNotFound(): Promise<never> {
throw new ErrorHttpResponse(
HttpStatusCode.NOT_FOUND,
'User not found',
'The requested user does not exist',
);
}

// Return string directly - sent as text response
@Get('/status')
public async getStatus(): Promise<string> {
return 'Service is healthy';
}
}

2. Native types (Advanced)

If you need direct access to adapter features, inject native types with @Response() and @Next(). In this mode you're responsible for the full response lifecycle: setting headers, choosing the status code, sending the body (or streaming), and deciding whether to call next() to continue the pipeline. This trades portability for full control and is useful for advanced scenarios like fine-grained caching headers, streaming, or integrating with middleware that expects native objects.

Usage with different adapters:

@Controller('/message')
export class ResponseExpressController {
@Get()
public async sendMessage(
@Response() response: express.Response,
): Promise<void> {
response.send({ message: 'hello' });
}
}
When to use native types

Reach for native types when you need precise control over the HTTP exchange—streaming large payloads, fine-tuning caching and headers, squeezing out adapter-specific performance, or interoperating with middleware that expects a native response object. For most routes, the framework-agnostic approach remains simpler, more portable, and easier to test.

Important: Choose one approach per route

Avoid mixing modes within the same route. If you inject @Response() or @Next(), your handler should manage the response entirely and typically return void (or the framework Response for Hono). Conversely, when you return values for Inversify to send, don't also write to the native response object.

Inheritance

Controllers support inheritance, allowing you to define base controllers with common route handlers that can be reused and optionally overridden in derived controllers. This promotes code reuse and consistency across similar endpoints.

abstract class BaseResourceController {
@Get()
public async list(): Promise<Resource[]> {
return [
{ id: 1, name: 'Resource 1' },
{ id: 2, name: 'Resource 2' },
];
}

@Get('/:id')
public async getById(
@Params({
name: 'id',
})
id: string,
): Promise<Resource> {
return { id: parseInt(id), name: `Resource ${id}` };
}
}

@Controller('/users')
export class UsersController extends BaseResourceController {}

@Controller('/products')
export class ProductsController extends BaseResourceController {
// Override the list method with custom implementation
@Get()
public override async list(): Promise<Resource[]> {
return [
{ id: 1, name: 'Product A' },
{ id: 2, name: 'Product B' },
{ id: 3, name: 'Product C' },
];
}
}

How inheritance works

When a controller extends a base class:

  • All route handlers from the base controller are inherited - The derived controller automatically includes all HTTP method decorators (@Get(), @Post(), etc.) from its parent classes
  • Routes can be overridden - If a derived controller defines a route with the same HTTP method and path as its base class, the derived implementation takes precedence
  • Base path applies to all routes - The @Controller() path specified on the derived class applies to both inherited and overridden routes
  • Metadata walks up the prototype chain - The framework traverses the entire inheritance hierarchy to collect all route metadata
Route collision resolution

When a derived controller defines a route with the same HTTP method and path as its parent, the derived controller's implementation takes precedence. This allows you to selectively customize behavior while keeping the base implementation for other routes.