HTTP Layer
The HTTP Layer handles web requests using createHttpServer(). It attaches to the Application Layerโs DI container, giving your routes access to all registered services while providing routing, middleware, and request/response handling.
Overview
The HTTP Layer provides:
- ๐ Routing - Define GET, POST, PUT, DELETE, PATCH endpoints
- ๐ Middleware - Request/response processing pipeline
- ๐ Request Validation - Automatic schema validation
- ๐ค Response Formatting - JSON, HTML, streaming, file downloads
- ๐ Security - CORS, rate limiting, authentication
- ๐ Error Handling - Centralized error responses
Creating an HTTP Server
Basic Server
import { createApp } from '@bunty/common';
import { createHttpServer } from '@bunty/http';
const app = createApp({
name: 'api',
providers: [UserService, OrderService],
});
const http = createHttpServer(app, {
port: 3000,
});
http.get('/health', (req, res) => {
return res.json({ status: 'ok' });
});
await app.start();
await http.start();
Terminal output:
[2025/10/30 19:53:14] [INFO] [HTTP] Server starting on port 3000โฆ
With Options
const http = createHttpServer(app, {
port: env('PORT', 3000),
host: env('HOST', '0.0.0.0'),
routes: './routes/**/*.ts', // Auto-load routes
cors: {
origin: '*',
methods: ['GET', 'POST', 'PUT', 'DELETE'],
},
rateLimit: {
windowMs: 15 * 60 * 1000, // 15 minutes
max: 100, // Max 100 requests per window
},
});
Routing
Basic Routes
import { createHttpServer } from '@bunty/http';
const http = createHttpServer(app, { port: 3000 });
// GET request
http.get('/users', async (req, res) => {
const users = await userService.findAll();
return res.json(users);
});
// POST request
http.post('/users', async (req, res) => {
const user = await userService.create(req.body);
return res.status(201).json(user);
});
// PUT request
http.put('/users/:id', async (req, res) => {
const user = await userService.update(req.params.id, req.body);
return res.json(user);
});
// DELETE request
http.delete('/users/:id', async (req, res) => {
await userService.delete(req.params.id);
return res.status(204).send();
});
// PATCH request
http.patch('/users/:id', async (req, res) => {
const user = await userService.patch(req.params.id, req.body);
return res.json(user);
});
Route Parameters
// URL parameters
http.get('/users/:id', async (req, res) => {
const userId = req.params.id;
const user = await userService.find(userId);
return res.json(user);
});
// Multiple parameters
http.get('/posts/:postId/comments/:commentId', async (req, res) => {
const { postId, commentId } = req.params;
const comment = await commentService.find(postId, commentId);
return res.json(comment);
});
// Query parameters
http.get('/users', async (req, res) => {
const { page = 1, limit = 10, search } = req.query;
const users = await userService.paginate({ page, limit, search });
return res.json(users);
});
Using Services in Routes
import { inject } from '@bunty/common';
http.get('/api/orders/:id', async (req, res) => {
// Inject service from DI container
const orderService = inject(OrderService);
const order = await orderService.find(req.params.id);
if (!order) {
return res.status(404).json({ error: 'Order not found' });
}
return res.json(order);
});
http.post('/api/orders', async (req, res) => {
const orderService = inject(OrderService);
const paymentService = inject(PaymentService);
const emailService = inject(EmailService);
// Create order
const order = await orderService.create(req.body);
// Process payment
await paymentService.charge(order);
// Send confirmation
await emailService.sendOrderConfirmation(order);
return res.status(201).json(order);
});
Middleware
Middleware runs before route handlers:
Global Middleware
import { Logger } from '@bunty/common';
const logger = new Logger('HTTP');
// Logging middleware
http.use((req, res, next) => {
const start = Date.now();
logger.info(`${req.method} ${req.path}`);
res.on('finish', () => {
const duration = Date.now() - start;
logger.success(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
});
next();
});
// Authentication middleware
http.use(async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
try {
const authService = inject(AuthService);
req.user = await authService.verifyToken(token);
next();
} catch (error) {
return res.status(401).json({ error: 'Invalid token' });
}
});
// CORS middleware
http.use((req, res, next) => {
res.setHeader('Access-Control-Allow-Origin', '*');
res.setHeader('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE');
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization');
next();
});
Route-Specific Middleware
// Auth middleware for specific routes
const requireAuth = async (req, res, next) => {
const authService = inject(AuthService);
if (!req.headers.authorization) {
return res.status(401).json({ error: 'Unauthorized' });
}
const user = await authService.verify(req.headers.authorization);
req.user = user;
next();
};
// Apply to specific routes
http.get('/api/profile', requireAuth, async (req, res) => {
return res.json(req.user);
});
http.post('/api/orders', requireAuth, async (req, res) => {
const orderService = inject(OrderService);
const order = await orderService.create({
...req.body,
userId: req.user.id,
});
return res.status(201).json(order);
});
Admin Middleware
const requireAdmin = async (req, res, next) => {
if (!req.user || !req.user.isAdmin) {
return res.status(403).json({ error: 'Forbidden' });
}
next();
};
// Protected admin routes
http.delete('/api/users/:id', requireAuth, requireAdmin, async (req, res) => {
const userService = inject(UserService);
await userService.delete(req.params.id);
return res.status(204).send();
});
Request Validation
Validate request bodies using schemas:
import { t } from '@bunty/common';
const CreateUserSchema = t.Object({
email: t.String().Email(),
password: t.String().MinLength(8),
name: t.String().MinLength(2),
});
http.post('/api/users', async (req, res) => {
// Validate request body
const validation = CreateUserSchema.validate(req.body);
if (!validation.success) {
return res.status(400).json({
error: 'Validation failed',
details: validation.errors,
});
}
const userService = inject(UserService);
const user = await userService.create(validation.data);
return res.status(201).json(user);
});
Validation Middleware
const validate = (schema) => {
return (req, res, next) => {
const result = schema.validate(req.body);
if (!result.success) {
return res.status(400).json({
error: 'Validation failed',
details: result.errors,
});
}
req.validatedData = result.data;
next();
};
};
// Use validation middleware
http.post('/api/users', validate(CreateUserSchema), async (req, res) => {
const userService = inject(UserService);
const user = await userService.create(req.validatedData);
return res.status(201).json(user);
});
Response Handling
JSON Responses
http.get('/api/users', async (req, res) => {
const users = await userService.findAll();
return res.json(users);
});
http.get('/api/users/:id', async (req, res) => {
const user = await userService.find(req.params.id);
return res.status(200).json(user);
});
Error Responses
http.get('/api/users/:id', async (req, res) => {
const user = await userService.find(req.params.id);
if (!user) {
return res.status(404).json({
error: 'User not found',
code: 'USER_NOT_FOUND',
});
}
return res.json(user);
});
File Downloads
http.get('/api/exports/users.csv', async (req, res) => {
const exportService = inject(ExportService);
const csv = await exportService.generateUsersCsv();
res.setHeader('Content-Type', 'text/csv');
res.setHeader('Content-Disposition', 'attachment; filename=users.csv');
return res.send(csv);
});
Streaming Responses
http.get('/api/stream/logs', async (req, res) => {
res.setHeader('Content-Type', 'text/event-stream');
res.setHeader('Cache-Control', 'no-cache');
res.setHeader('Connection', 'keep-alive');
const logService = inject(LogService);
const stream = logService.streamLogs();
stream.on('data', (log) => {
res.write(`data: ${JSON.stringify(log)}\n\n`);
});
stream.on('end', () => {
res.end();
});
});
Error Handling
Global Error Handler
import { Logger } from '@bunty/common';
const logger = new Logger('ErrorHandler');
http.use((error, req, res, next) => {
logger.error('Request failed:', {
method: req.method,
path: req.path,
error: error.message,
stack: error.stack,
});
// Production error response
if (env('NODE_ENV') === 'production') {
return res.status(500).json({
error: 'Internal server error',
});
}
// Development error response
return res.status(500).json({
error: error.message,
stack: error.stack,
});
});
Try-Catch Pattern
http.post('/api/orders', async (req, res) => {
try {
const orderService = inject(OrderService);
const order = await orderService.create(req.body);
return res.status(201).json(order);
} catch (error) {
if (error.code === 'VALIDATION_ERROR') {
return res.status(400).json({ error: error.message });
}
if (error.code === 'PAYMENT_FAILED') {
return res.status(402).json({ error: 'Payment processing failed' });
}
throw error; // Let global handler deal with it
}
});
Complete Example
Hereโs a full REST API with all patterns:
import { createApp, Injectable, inject, Logger, t, env } from '@bunty/common';
import { createHttpServer } from '@bunty/http';
// Schema
const CreateOrderSchema = t.Object({
items: t.Array(t.Object({
productId: t.Number(),
quantity: t.Number().Min(1),
})),
shippingAddress: t.String(),
});
// Services
@Injectable()
class OrderService {
private logger = new Logger('OrderService');
constructor(
private db: DatabaseService,
private payment: PaymentService
) {}
async create(data: any, userId: number) {
this.logger.info('Creating order for user:', userId);
const order = await this.db.insert(ordersTable, {
...data,
userId,
status: 'pending',
});
await this.payment.charge(order);
this.logger.success('Order created:', order.id);
return order;
}
async findByUser(userId: number) {
return await this.db.query(ordersTable)
.where('userId', '=', userId)
.findMany();
}
}
// Application
const app = createApp({
name: 'ecommerce-api',
providers: [OrderService, PaymentService, DatabaseService, AuthService],
});
// HTTP Server
const http = createHttpServer(app, {
port: env('PORT', 3000),
});
const logger = new Logger('HTTP');
// Middleware
http.use((req, res, next) => {
const start = Date.now();
logger.info(`${req.method} ${req.path}`);
res.on('finish', () => {
const duration = Date.now() - start;
logger.success(`${req.method} ${req.path} ${res.statusCode} ${duration}ms`);
});
next();
});
const requireAuth = async (req, res, next) => {
const token = req.headers.authorization?.replace('Bearer ', '');
if (!token) {
return res.status(401).json({ error: 'Unauthorized' });
}
const authService = inject(AuthService);
req.user = await authService.verify(token);
next();
};
// Routes
http.get('/health', (req, res) => {
return res.json({ status: 'ok' });
});
http.get('/api/orders', requireAuth, async (req, res) => {
const orderService = inject(OrderService);
const orders = await orderService.findByUser(req.user.id);
return res.json(orders);
});
http.post('/api/orders', requireAuth, async (req, res) => {
const validation = CreateOrderSchema.validate(req.body);
if (!validation.success) {
return res.status(400).json({
error: 'Validation failed',
details: validation.errors,
});
}
try {
const orderService = inject(OrderService);
const order = await orderService.create(validation.data, req.user.id);
return res.status(201).json(order);
} catch (error) {
logger.error('Order creation failed:', error);
return res.status(500).json({ error: 'Failed to create order' });
}
});
// Start
await app.start();
await http.start();
Best Practices
โ Do
- Use dependency injection for services
- Validate all request inputs
- Handle errors gracefully
- Use middleware for cross-cutting concerns
- Log all requests and errors
- Return appropriate HTTP status codes
- Use async/await for route handlers
- Separate business logic into services
โ Donโt
- Put business logic in route handlers
- Skip input validation
- Return raw error messages to clients
- Create services outside DI container
- Block the event loop with sync operations
- Ignore error handling
- Use magic numbers for status codes
- Mix HTTP concerns with business logic
๐ HTTP Layer Summary
The HTTP Layer handles web requests using createHttpServer(). It provides routing, middleware, validation, and error handling while accessing services from the Application Layerโs DI container. Build REST APIs, GraphQL endpoints, or any HTTP-based service with full access to your applicationโs business logic and state.
Next Steps
- Learn about the Worker Layer for background tasks
- Understand Application Architecture overview
- See Request Validation for advanced validation patterns
- Explore Dependency Injection for service management