HTTP
HTTP support lives in @bunito/http. Add it when an application needs routes, middleware, request injections, JSON handling, CORS, response handling, or HTTP exceptions. Controllers use the core @Controller() decorator from @bunito/bunito; route decorators and HTTP runtime pieces come from @bunito/http.
Installation
Install the HTTP package:
bun add @bunito/httpIf you want schema-backed validation, add Zod:
bun add zodEnable HTTP
Import HTTPModule in the application module:
import { LoggerModule, Module } from '@bunito/bunito';
import { HTTPModule } from '@bunito/http';
@Module({
imports: [LoggerModule, HTTPModule],
})
class AppModule {}The HTTP package uses Bun server integration from @bunito/bun and registers routes from controller metadata. HTTPModule imports the Bun server module automatically. If the module graph already contains a Bun server module, that existing module is used instead.
Controllers
Controllers are classes decorated with @Controller():
import { Controller, Logger, Provider } from '@bunito/bunito';
import { Get } from '@bunito/http';
@Provider()
class UsersService {
findAll(): string[] {
return ['Ada', 'Grace'];
}
}
@Controller('/users', {
injects: [UsersService, Logger],
})
class UsersController {
constructor(
private readonly usersService: UsersService,
private readonly logger: Logger,
) {}
@Get()
list(): Response {
this.logger.debug('list() called');
return Response.json({
users: this.usersService.findAll(),
});
}
}Register the controller in a module:
@Module({
imports: [LoggerModule, HTTPModule],
providers: [UsersService],
controllers: [UsersController],
})
class AppModule {}Routes
Use route decorators for HTTP methods:
import { Delete, Get, Head, OnRequest, Patch, Post, Put } from '@bunito/http';Available decorators include Get, Head, Post, Put, Patch, Delete, and OnRequest. Use OnRequest when a handler should match any HTTP method. OPTIONS is handled by the router for discovered routes, which is especially useful for CORS preflight requests.
Request Injections
HTTP handlers can receive request data through explicit injections:
import { Controller } from '@bunito/bunito';
import { Get, Params, Query } from '@bunito/http';
import { z } from 'zod';
const ParamsSchema = z.object({
id: z.string(),
});
const QuerySchema = z.object({
include: z.string().default('summary'),
});
@Controller('/users')
class UsersController {
@Get('/:id', {
injects: [Params(ParamsSchema), Query(QuerySchema)],
})
getUser(
params: Params<typeof ParamsSchema>,
query: Query<typeof QuerySchema>,
): Response {
return Response.json({ params, query });
}
}Important injections:
Params: route paramsQuery: URL query valuesBody: parsed request bodyMethod: HTTP methodContext: the current HTTP contextCustomInjection: a custom resolver that can read the HTTP context
Use CustomInjection when a handler needs data that is not covered by the bundled injections:
import { CustomInjection, Get } from '@bunito/http';
const TraceId = CustomInjection((context) => context.request.headers.get('x-trace-id'));
class UsersController {
@Get('/', {
injects: [TraceId],
})
list(traceId: string | null): Response {
return Response.json({ traceId });
}
}JSON Middleware
Use JSONSerializer and BodyParser to work with plain object responses and JSON bodies. HTTPModule imports the bundled parser and serializer providers:
import { Controller, LoggerModule, Module } from '@bunito/bunito';
import {
Body,
BodyParser,
HTTPModule,
JSONSerializer,
Post,
UseMiddleware,
} from '@bunito/http';
import { z } from 'zod';
const BodySchema = z.object({
name: z.string(),
});
@Controller('/users')
@UseMiddleware(JSONSerializer)
@UseMiddleware(BodyParser, { parser: 'json' })
class UsersController {
@Post('/', {
injects: [Body(BodySchema)],
})
createUser(body: Body<typeof BodySchema>): Record<string, unknown> {
return {
created: true,
body,
};
}
}
@Module({
imports: [LoggerModule, HTTPModule],
controllers: [UsersController],
})
class AppModule {}UseMiddleware accepts one middleware class and optional middleware options. Apply the decorator more than once when a controller or module needs several middleware.
Prefixes and Middleware
UsePrefix, UseMiddleware and UseCORS can be applied at module level. Module-level decorators affect controllers declared in that module and in imported child modules:
import { Module, UsePrefix } from '@bunito/bunito';
import {
HTTPModule,
JSONSerializer,
UseCORS,
UseMiddleware,
} from '@bunito/http';
@Module({
imports: [HTTPModule],
controllers: [UsersController],
})
@UsePrefix('/api')
@UseMiddleware(JSONSerializer)
@UseCORS({
origin: 'https://example.com',
methods: ['GET', 'POST'],
allowedHeaders: ['X-Trace-Id'],
credentials: true,
maxAge: 3600,
})
class ApiModule {}This keeps route groups, middleware, response handling, and CORS policy local to a feature module.
CORS and Headers
Use UseCORS on a module or controller to configure CORS for its routes:
import { Controller, Module, UsePrefix } from '@bunito/bunito';
import { Get, HTTPModule, UseCORS } from '@bunito/http';
@Controller()
@UseCORS({
methods: ['GET'],
})
class FooController {
@Get()
getFoo(): Response {
return Response.json({
foo: 'Hello foo!',
});
}
}
@Module({
imports: [HTTPModule],
controllers: [FooController],
})
@UsePrefix('/foo')
@UseCORS({
origin: '*',
credentials: false,
maxAge: 3600,
})
class FooModule {}CORS options are merged from parent modules to feature modules and controllers, with more local options overriding earlier ones.
For browser clients, use an explicit origin when credentials is enabled.
Middleware may also implement beforeResponse() to adjust the response after a route handler runs. This is the recommended place for feature-local response headers.
Exceptions
HTTP exceptions are exported from @bunito/http:
import { NotFoundException } from '@bunito/http';
throw new NotFoundException();Use them when a handler needs to stop normal response flow with an HTTP error.
Testing
HTTP tests usually replace the real Bun server with Test.BunServerModule from @bunito/bun. Because HTTPModule respects an already registered server module, put Test.BunServerModule before HTTPModule in the test app imports:
import '@bunito/bun';
import { App } from '@bunito/bunito';
import { HTTPModule } from '@bunito/http';
import { Test } from '@bunito/testing';
const app = await App.start({
imports: [Test.BunServerModule, Test.LoggerModule, HTTPModule, AppModule],
});
const response = await Test.bunServer
.buildRequest('/users/ada')
.withMethod('GET')
.send();
expect(response.status).toBe(200);
await app.shutdown();Test.bunServer.buildRequest() returns a small request builder with withMethod(), withHeaders(), withBody(), build(), and send(). The test server simulates Bun route tables, including :param segments and trailing * wildcards, so route params are available to the HTTP router just like they are at runtime.
Tutorials
Build these ideas step by step:
