๐Ÿ“ฆ
Bunty

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โ€ฆ
[2025/10/30 19:53:15] [SUCCESS] [HTTP] Server listening on http://localhost: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

Have questions? Join our Discord community
Found an issue? Edit this page on GitHub