πŸ“¦
Bunty

Application Layer

The Application Layer is the foundation of every Bunty application. Created with createApp(), it initializes the dependency injection container, loads configuration, manages service lifecycle, and provides the shared context for HTTP servers, workers, and headless operations.

Overview

Think of the Application Layer as the brain of your application:

  • 🧠 Dependency Injection - Manages service creation and injection
  • βš™οΈ Configuration - Loads and validates environment config
  • πŸ”„ Lifecycle - Controls startup, runtime, and shutdown
  • πŸ“¦ Service Registry - Central registry for all services
  • πŸ“ Logging - Structured logging infrastructure

Creating an Application

Basic Application

import { createApp } from '@bunty/common';

const app = createApp({
    name: 'my-app',
    providers: [
        UserService,
        OrderService,
        DatabaseService,
    ],
});

await app.start();

With Configuration

import { createApp } from '@bunty/common';

const app = createApp({
    name: 'my-app',
    providers: [UserService, OrderService],
    config: {
        database: {
            host: env('DB_HOST'),
            port: env('DB_PORT', 5432),
        },
        app: {
            debug: env('DEBUG', false),
        },
    },
});

await app.start();

Application Options

interface AppOptions {
    name: string;                    // Application name
    providers?: Injectable[];        // Services to register
    config?: Record<string, any>;    // Configuration object
    logLevel?: LogFlags;             // Logging configuration
    onInit?: () => Promise<void>;    // Initialization hook
    onReady?: () => Promise<void>;   // Ready hook
    onShutdown?: () => Promise<void>; // Shutdown hook
}

Lifecycle Hooks

The Application Layer provides three lifecycle hooks:

onInit

Called first, before services are instantiated:

const app = createApp({
    name: 'my-app',
    providers: [DatabaseService, CacheService],
    
    async onInit() {
        console.log('Application initializing...');
        // Load environment variables
        // Validate configuration
        // Set up logging
    },
});

onReady

Called after all services are initialized:

const app = createApp({
    name: 'my-app',
    providers: [DatabaseService, CacheService],
    
    async onReady() {
        console.log('Application ready!');
        // Log startup metrics
        // Send health check
        // Warm up caches
    },
});

onShutdown

Called during graceful shutdown:

const app = createApp({
    name: 'my-app',
    providers: [DatabaseService, CacheService],
    
    async onShutdown() {
        console.log('Application shutting down...');
        // Close database connections
        // Flush queues
        // Save state
    },
});

Complete Lifecycle Example

import { createApp, Injectable, Logger } from '@bunty/common';

const logger = new Logger('App');

const app = createApp({
    name: 'ecommerce-api',
    providers: [
        DatabaseService,
        CacheService,
        UserService,
        OrderService,
    ],
    
    async onInit() {
        logger.info('Initializing application...');
        
        // Validate environment
        const requiredEnvVars = ['DB_HOST', 'REDIS_HOST', 'API_KEY'];
        for (const envVar of requiredEnvVars) {
            if (!env(envVar)) {
                throw new Error(`Missing required env var: ${envVar}`);
            }
        }
        
        logger.success('Environment validated');
    },
    
    async onReady() {
        logger.info('Application ready!');
        
        // Get services from container
        const db = app.container.get(DatabaseService);
        const cache = app.container.get(CacheService);
        
        // Log connection status
        logger.success('Database connected:', await db.isConnected());
        logger.success('Cache connected:', await cache.isConnected());
        
        // Warm up cache
        await cache.warmUp();
        logger.success('Cache warmed up');
    },
    
    async onShutdown() {
        logger.warn('Shutting down gracefully...');
        
        const db = app.container.get(DatabaseService);
        const cache = app.container.get(CacheService);
        
        // Close connections
        await db.disconnect();
        await cache.disconnect();
        
        logger.success('Cleanup complete');
    },
});

await app.start();

Terminal output:

[2025/10/30 19:53:14] [INFO] [App] Initializing application…
[2025/10/30 19:53:15] [SUCCESS] [App] Environment validated
[2025/10/30 19:53:16] [INFO] [App] Application ready!
[2025/10/30 19:53:17] [SUCCESS] [App] Database connected: true
[2025/10/30 19:53:18] [SUCCESS] [App] Cache connected: true
[2025/10/30 19:53:19] [SUCCESS] [App] Cache warmed up

Dependency Injection Container

The DI container manages all service instances:

Registering Services

import { createApp, Injectable } from '@bunty/common';

@Injectable()
class UserService {
    async getUser(id: number) {
        return { id, name: 'John Doe' };
    }
}

@Injectable()
class OrderService {
    constructor(private userService: UserService) {}
    
    async getUserOrders(userId: number) {
        const user = await this.userService.getUser(userId);
        return user.orders;
    }
}

const app = createApp({
    name: 'my-app',
    providers: [UserService, OrderService],
});

await app.start();

Getting Services from Container

// After app.start()
const userService = app.container.get(UserService);
const user = await userService.getUser(123);

const orderService = app.container.get(OrderService);
const orders = await orderService.getUserOrders(123);

Manual Registration

const app = createApp({ name: 'my-app' });

// Register services manually
app.container.register(UserService);
app.container.register(OrderService);
app.container.register(DatabaseService, {
    lifetime: 'singleton',
});

await app.start();

Configuration Management

The Application Layer centralizes configuration:

Loading Configuration

import { createApp, env } from '@bunty/common';

const app = createApp({
    name: 'my-app',
    config: {
        database: {
            host: env('DB_HOST', 'localhost'),
            port: env('DB_PORT', 5432),
            name: env('DB_NAME', 'myapp'),
        },
        redis: {
            host: env('REDIS_HOST', 'localhost'),
            port: env('REDIS_PORT', 6379),
        },
        app: {
            port: env('PORT', 3000),
            debug: env('DEBUG', false),
            logLevel: env('LOG_LEVEL', 'info'),
        },
    },
});

Accessing Configuration

@Injectable()
class DatabaseService {
    constructor() {
        const config = app.config.database;
        this.connect(config.host, config.port, config.name);
    }
}

Type-Safe Configuration

import { t } from '@bunty/common';

const ConfigSchema = t.Object({
    database: t.Object({
        host: t.String(),
        port: t.Number(),
        name: t.String(),
    }),
    app: t.Object({
        port: t.Number(),
        debug: t.Boolean(),
    }),
});

const app = createApp({
    name: 'my-app',
    config: ConfigSchema.parse({
        database: {
            host: env('DB_HOST'),
            port: env('DB_PORT', 5432),
            name: env('DB_NAME'),
        },
        app: {
            port: env('PORT', 3000),
            debug: env('DEBUG', false),
        },
    }),
});

Headless Execution

Run application logic without HTTP or workers:

One-Time Execution

import { createApp } from '@bunty/common';

const app = createApp({
    name: 'migration-tool',
    providers: [DatabaseService, MigrationService],
});

await app.start(async (container) => {
    const migrator = container.get(MigrationService);
    await migrator.run();
    await app.shutdown();
});

CLI Tool

import { createApp } from '@bunty/common';

const app = createApp({
    name: 'import-tool',
    providers: [DatabaseService, ImportService],
});

await app.start(async (container) => {
    const importer = container.get(ImportService);
    
    const file = process.argv[2];
    if (!file) {
        console.error('Usage: bun run import.ts <file>');
        process.exit(1);
    }
    
    await importer.importFromFile(file);
    console.log('Import completed!');
    
    await app.shutdown();
});

Usage:

bun run import.ts data.csv

ETL Pipeline

import { createApp, Injectable, Logger } from '@bunty/common';

@Injectable()
class ETLService {
    private logger = new Logger('ETL');
    
    constructor(
        private source: SourceService,
        private transform: TransformService,
        private target: TargetService
    ) {}
    
    async run() {
        this.logger.info('Starting ETL pipeline...');
        
        // Extract
        const data = await this.source.extract();
        this.logger.success('Extracted records:', data.length);
        
        // Transform
        const transformed = await this.transform.process(data);
        this.logger.success('Transformed records:', transformed.length);
        
        // Load
        await this.target.load(transformed);
        this.logger.success('Loaded records to target');
    }
}

const app = createApp({
    name: 'etl-pipeline',
    providers: [ETLService, SourceService, TransformService, TargetService],
});

await app.start(async (container) => {
    const etl = container.get(ETLService);
    await etl.run();
    await app.shutdown();
});

Graceful Shutdown

Handle shutdown signals properly:

import { createApp } from '@bunty/common';

const app = createApp({
    name: 'my-app',
    providers: [...],
    
    async onShutdown() {
        console.log('Cleaning up resources...');
        // Close connections, flush queues, etc.
    },
});

await app.start();

// Handle shutdown signals
process.on('SIGINT', async () => {
    console.log('Received SIGINT, shutting down...');
    await app.shutdown();
    process.exit(0);
});

process.on('SIGTERM', async () => {
    console.log('Received SIGTERM, shutting down...');
    await app.shutdown();
    process.exit(0);
});

Sharing Across Layers

The Application Layer provides context to HTTP and Worker layers:

import { createApp } from '@bunty/common';
import { createHttpServer } from '@bunty/http';
import { createWorker } from '@bunty/worker';

// 1. Create application
const app = createApp({
    name: 'full-stack-app',
    providers: [
        UserService,
        OrderService,
        EmailService,
        DatabaseService,
    ],
});

// 2. HTTP layer uses app services
const http = createHttpServer(app, { port: 3000 });

http.get('/api/users/:id', async (req, res) => {
    const userService = app.container.get(UserService);
    const user = await userService.getUser(req.params.id);
    return res.json(user);
});

// 3. Worker layer uses same services
const worker = createWorker({
    app,
    interval: '5m',
    run: async (container) => {
        const emailService = container.get(EmailService);
        await emailService.sendPendingEmails();
    },
});

// All layers share the same container
await app.start();
await http.start();
await worker.start();

Best Practices

βœ… Do

  • Register all services in providers array
  • Use lifecycle hooks for initialization/cleanup
  • Validate configuration in onInit()
  • Handle shutdown signals gracefully
  • Use type-safe configuration schemas
  • Keep Application Layer logic minimal
  • Delegate business logic to services

❌ Don’t

  • Put business logic in lifecycle hooks
  • Create services outside the container
  • Skip graceful shutdown handling
  • Mix HTTP/Worker logic with app initialization
  • Ignore configuration validation
  • Create multiple app instances unnecessarily

🧠 Application Layer Summary

The Application Layer is the foundation of every Bunty application. It manages the DI container, loads configuration, controls lifecycle, and provides shared context for HTTP servers, workers, and headless operations. Create once with createApp(), then attach any runtime behavior you need. Everything shares the same services, config, and state.

Next Steps

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