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:
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
providersarray - 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
- Explore the HTTP Layer for building APIs
- Learn about the Worker Layer for background tasks
- Understand Application Architecture overview
- See Dependency Injection for service patterns