Foundation Module
The Foundation module serves as the architectural backbone of Orchestr, providing the fundamental building blocks that power your application. Inspired by Laravel's Foundation namespace, this module manages the complete application lifecycle from initialization to termination.
The Foundation module consists of three primary components:
Application Class
The IoC container and application orchestrator that manages service bindings, provider registration, and bootstrap process
ServiceProvider Base Class
An abstract class that provides a consistent pattern for registering and bootstrapping application services
HTTP Kernel
The request handler that manages middleware pipeline and coordinates routing
Together, these components create a robust, Laravel-like architecture that brings elegant patterns and type safety to TypeScript applications.
Application Class
The Application class extends the Container class and serves as the heart of the framework. It acts as both an IoC (Inversion of Control) container and the orchestrator for the entire application lifecycle.
Initialization
When you create an Application instance, several initialization steps occur automatically:
- Base Path Configuration - Sets the root directory for path resolution
- Base Bindings Registration - Registers the application instance itself in the container
- Core Aliases Setup - Creates convenient aliases for accessing the application
import { Application } from '@orchestr-sh/orchestr';
// Using current directory as base path
const app = new Application(__dirname);
// Or explicitly set the base path
const app = new Application('/path/to/your/app');Core Methods
Service Provider Management
register(provider: ServiceProvider | ProviderClass): ServiceProvider
Registers a service provider with the application. The provider can be either an instance or a class constructor.
// Register with class constructor
app.register(DatabaseServiceProvider);
// Register with instance
app.register(new CacheServiceProvider(app));registerProviders(providers: Array<ServiceProvider | ProviderClass>): void
Bulk registration of multiple providers:
app.registerProviders([
DatabaseServiceProvider,
CacheServiceProvider,
QueueServiceProvider,
MailServiceProvider
]);boot(): Promise<void>
Boots all registered service providers by calling their boot() methods in registration order. This is an asynchronous operation that waits for each provider to complete booting before moving to the next.
await app.boot();Container Methods (Inherited)
Since Application extends Container, all container methods are available:
bind(abstract: any, concrete?: any): void
Register a binding in the container:
app.bind('config', () => new ConfigService());
app.bind(UserService, () => new UserService());singleton(abstract: any, concrete?: any): void
Register a shared binding (only one instance will be created):
app.singleton('logger', () => new Logger());
app.singleton(DatabaseConnection, () => new DatabaseConnection());make<T>(abstract: any): T
Resolve a service from the container:
const logger = app.make<Logger>('logger');
const userService = app.make(UserService);Path Helpers
The Application class provides convenient methods for accessing common directories within your application structure.
path(path?: string): string
Get path to the application directory:
app.path(); // /path/to/your/app/app
app.path('Models'); // /path/to/your/app/app/ModelsconfigPath(path?: string): string
Get path to the configuration directory:
app.configPath(); // /path/to/your/app/config
app.configPath('database'); // /path/to/your/app/config/databasestoragePath(path?: string): string
Get path to the storage directory:
app.storagePath(); // /path/to/your/app/storage
app.storagePath('logs'); // /path/to/your/app/storage/logsEnvironment Management
The Application class provides methods for environment detection and configuration:
environment(): string
Get the current application environment:
const env = app.environment();
// Returns: 'development', 'production', 'staging', etc.
// Defaults to 'production' if NODE_ENV is not setisDebug(): boolean
Determine if the application is in debug mode:
if (app.isDebug()) {
console.log('Debug mode is enabled');
}
// Returns true if DEBUG environment variable is 'true'runningInConsole(): boolean
Determine if the application is running in the console:
if (app.runningInConsole()) {
console.log('Running in CLI mode');
}Service Providers
Service Providers are the central place for organizing and bootstrapping your application's services. They follow the same pattern as Laravel's service providers, providing a consistent and organized way to register services with the IoC container.
Purpose and Role
Service Providers serve several critical purposes:
- Centralized Service Registration - All service bindings are organized in provider classes rather than scattered throughout the codebase
- Dependency Organization - Related services can be grouped together (e.g., all database-related bindings in a DatabaseServiceProvider)
- Lifecycle Management - Separate registration from bootstrapping through distinct
register()andboot()methods - Lazy Loading - Services are only instantiated when needed, not at registration time
- Testability - Providers can be easily mocked or swapped for testing
Creating Service Providers
Every service provider must extend the ServiceProvider abstract class and implement the register() method.
Basic Service Provider
import { ServiceProvider } from '@orchestr-sh/orchestr';
import { ConfigService } from '../Services/ConfigService';
export class ConfigServiceProvider extends ServiceProvider {
/**
* Register services in the container
*/
register(): void {
this.app.singleton('config', () => {
return new ConfigService(this.app.configPath());
});
}
/**
* Bootstrap services (optional)
*/
boot(): void {
const config = this.app.make<ConfigService>('config');
config.load();
}
}Service Provider with Dependencies
import { ServiceProvider } from '@orchestr-sh/orchestr';
import { DatabaseConnection } from '../Database/DatabaseConnection';
import { ConfigService } from '../Services/ConfigService';
export class DatabaseServiceProvider extends ServiceProvider {
register(): void {
// Register database connection as a singleton
this.app.singleton(DatabaseConnection, () => {
const config = this.app.make<ConfigService>('config');
const dbConfig = config.get('database');
return new DatabaseConnection(dbConfig);
});
// Create an alias for convenience
this.app.alias(DatabaseConnection, 'db');
}
async boot(): Promise<void> {
// Boot can be async for operations like establishing connections
const db = this.app.make<DatabaseConnection>('db');
await db.connect();
console.log('Database connection established');
}
}Registration vs Booting
Service Providers have two distinct phases:
Registration Phase
- Occurs first for all providers
- Only register bindings with the container
- Don't resolve services yet (they may not be available)
- Keep this phase lightweight
Booting Phase
- Occurs after all providers have registered
- Can resolve and use services from other providers
- Perform initialization that depends on other services
- Can be asynchronous
export class CacheServiceProvider extends ServiceProvider {
register(): void {
// ❌ BAD: Don't resolve services here
// const config = this.app.make('config'); // May not be available yet
// ✅ GOOD: Only register bindings
this.app.singleton('cache', () => new RedisCache());
}
boot(): void {
// ✅ GOOD: Can resolve services here
const config = this.app.make('config');
const cache = this.app.make('cache');
if (config.get('cache.enabled')) {
cache.connect();
}
}
}HTTP Kernel
The HTTP Kernel is responsible for handling HTTP requests and managing the middleware pipeline. It coordinates the flow of requests through various middleware layers before they reach your routes.
Request Lifecycle
When an HTTP request comes in, it follows this lifecycle:
- Request Reception - The kernel receives the incoming request
- Middleware Pipeline - Request passes through global and route-specific middleware
- Route Resolution - The router matches the request to a route
- Controller Execution - The controller method is executed
- Response Generation - Response is created and sent back through middleware
- Response Sending - Final response is sent to the client
Middleware Pipeline
The HTTP Kernel manages a stack of middleware that processes requests and responses:
import { Kernel } from '@orchestr-sh/orchestr';
import { Request, Response } from 'express';
export class HttpKernel extends Kernel {
/**
* The application's global HTTP middleware stack.
*/
protected middleware: any[] = [
// Example middleware classes/functions
'Authenticate',
'TrimStrings',
'TrustProxies',
];
/**
* The application's route middleware groups.
*/
protected middlewareGroups: { [key: string]: any[] } = {
'web': [
'EncryptCookies',
'AddQueuedCookiesToResponse',
'StartSession',
],
'api': [
'Throttle:60,1',
'Bindings',
],
};
/**
* The application's route middleware aliases.
*/
protected routeMiddleware: { [key: string]: any } = {
'auth': 'Authenticate',
'auth.basic': 'AuthenticateWithBasicAuth',
'guest': 'RedirectIfAuthenticated',
'throttle': 'ThrottleRequests',
};
}Examples
Creating an Application Instance
import { Application } from '@orchestr-sh/orchestr';
// Create application with default settings
const app = new Application();
// Create with custom base path
const app = new Application('/custom/path');
// The application is now ready to register providers and services
console.log('Application created');
console.log('Base path:', app.getBasePath());
console.log('Environment:', app.environment());Registering Service Providers
import { Application } from '@orchestr-sh/orchestr';
import { DatabaseServiceProvider } from './providers/DatabaseServiceProvider';
import { CacheServiceProvider } from './providers/CacheServiceProvider';
const app = new Application();
// Register individual providers
app.register(new DatabaseServiceProvider(app));
// Register with class constructor
app.register(CacheServiceProvider);
// Register multiple providers at once
app.registerProviders([
DatabaseServiceProvider,
CacheServiceProvider,
]);
// Boot all providers
await app.boot();
console.log('Application is ready!');Creating Custom Service Providers
import { ServiceProvider } from '@orchestr-sh/orchestr';
import { Logger } from '../Services/Logger';
import { Config } from '../Services/Config';
export class LoggingServiceProvider extends ServiceProvider {
register(): void {
// Register different logger implementations based on environment
this.app.singleton('logger', () => {
const config = this.app.make<Config>('config');
if (this.app.environment() === 'production') {
return new Logger({
level: 'error',
file: this.app.storagePath('logs/app.log')
});
} else {
return new Logger({
level: 'debug',
console: true
});
}
});
}
boot(): void {
const logger = this.app.make<Logger>('logger');
logger.info('Logging service initialized');
}
provides(): string[] {
return ['logger'];
}
}Environment Configuration
import { Application } from '@orchestr-sh/orchestr';
const app = new Application();
// Check current environment
console.log('Environment:', app.environment());
console.log('Debug mode:', app.isDebug());
console.log('Running in console:', app.runningInConsole());
// Environment-specific logic
if (app.environment() === 'development') {
// Development-specific setup
app.singleton('config', () => new DevelopmentConfig());
} else {
// Production setup
app.singleton('config', () => new ProductionConfig());
}
// Path helpers for configuration
const configPath = app.configPath('app.json');
console.log('Config path:', configPath);Complete Bootstrap Process
import { Application } from '@orchestr-sh/orchestr';
import { ConfigServiceProvider } from './providers/ConfigServiceProvider';
import { DatabaseServiceProvider } from './providers/DatabaseServiceProvider';
import { CacheServiceProvider } from './providers/CacheServiceProvider';
import { RouteServiceProvider } from './providers/RouteServiceProvider';
async function createApplication(): Promise<Application> {
// 1. Create application instance
const app = new Application(process.cwd());
// 2. Register all service providers
app.registerProviders([
ConfigServiceProvider,
DatabaseServiceProvider,
CacheServiceProvider,
RouteServiceProvider,
]);
// 3. Boot all providers
await app.boot();
// 4. Register termination handlers
app.terminating(() => {
console.log('Application shutting down...');
// Cleanup resources here
});
return app;
}
// Usage
createApplication()
.then(app => {
console.log('Application ready!');
// Application is now ready to handle requests
// or run console commands
})
.catch(error => {
console.error('Failed to create application:', error);
process.exit(1);
});Best Practices
1. Organize Providers by Feature
Group related services in dedicated providers:
// ✅ GOOD - Feature-based organization
providers/
├── AppServiceProvider.ts # Core application services
├── AuthServiceProvider.ts # Authentication services
├── DatabaseServiceProvider.ts # Database connections and repositories
├── CacheServiceProvider.ts # Caching layer
└── NotificationServiceProvider.ts # Notifications
// ❌ AVOID - One massive provider
providers/
└── EverythingServiceProvider.ts # 1000+ lines of mixed responsibilities2. Keep Registration Phase Lightweight
// ✅ GOOD - Lightweight registration
register(): void {
this.app.singleton('cache', () => new RedisCache());
}
boot(): void {
const cache = this.app.make('cache');
cache.connect(); // Expensive operation in boot phase
}
// ❌ BAD - Heavy registration
register(): void {
const cache = new RedisCache(); // Heavy object creation
cache.connect(); // Network I/O in registration
this.app.instance('cache', cache);
}3. Use Environment-Specific Bindings
register(): void {
// Environment-aware service registration
this.app.singleton('logger', () => {
if (this.app.environment() === 'testing') {
return new NullLogger();
} else if (this.app.environment() === 'development') {
return new ConsoleLogger();
} else {
return new FileLogger(this.app.storagePath('logs'));
}
});
}4. Leverage Path Helpers
// ✅ GOOD - Use path helpers
register(): void {
this.app.singleton('config', () => {
return new ConfigService(this.app.configPath());
});
}
// ❌ BAD - Hardcoded paths
register(): void {
this.app.singleton('config', () => {
return new ConfigService('/path/to/app/config'); // Brittle
});
}5. Handle Asynchronous Operations in Boot
// ✅ GOOD - Async boot operations
async boot(): Promise<void> {
const db = this.app.make('database');
await db.connect();
const cache = this.app.make('cache');
await cache.warmup();
}
// ❌ BAD - Blocking sync operations
boot(): void {
const db = this.app.make('database');
db.connectSync(); // Blocks entire bootstrap
}6. Use Provider Contracts
export interface ServiceProviderContract {
register(): void;
boot?(): void | Promise<void>;
provides?(): string[];
}
export class DatabaseServiceProvider
extends ServiceProvider
implements ServiceProviderContract {
register(): void {
// Implementation
}
boot(): Promise<void> {
// Implementation
}
provides(): string[] {
return ['database', 'db.connection'];
}
}7. Register Termination Handlers
// In your bootstrap or a dedicated provider
app.terminating(async () => {
// Graceful shutdown
const db = app.make('database');
await db.disconnect();
const cache = app.make('cache');
await cache.flush();
console.log('Application shutdown complete');
});
process.on('SIGINT', () => app.terminate());
process.on('SIGTERM', () => app.terminate());Conclusion
The Foundation module provides the architectural backbone that makes Orchestr applications robust, maintainable, and scalable. By leveraging the Application class, Service Providers, and HTTP Kernel, you can build applications that follow proven patterns while taking advantage of TypeScript's type safety.
Key Benefits:
- Structured Architecture - Clear separation of concerns and responsibilities
- Dependency Injection - Automatic resolution and management of service dependencies
- Environment Awareness - Built-in support for different deployment environments
- Testability - Easy mocking and swapping of services for testing
- Scalability - Provider-based architecture supports growth and complexity
- Type Safety - Full TypeScript support throughout the foundation
By understanding and properly using these Foundation components, you'll be able to build sophisticated Node.js applications that are both powerful and maintainable.