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:

  1. Base Path Configuration - Sets the root directory for path resolution
  2. Base Bindings Registration - Registers the application instance itself in the container
  3. 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/Models

configPath(path?: string): string

Get path to the configuration directory:

app.configPath();           // /path/to/your/app/config
app.configPath('database'); // /path/to/your/app/config/database

storagePath(path?: string): string

Get path to the storage directory:

app.storagePath();        // /path/to/your/app/storage
app.storagePath('logs');  // /path/to/your/app/storage/logs

Environment 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 set

isDebug(): 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:

  1. Centralized Service Registration - All service bindings are organized in provider classes rather than scattered throughout the codebase
  2. Dependency Organization - Related services can be grouped together (e.g., all database-related bindings in a DatabaseServiceProvider)
  3. Lifecycle Management - Separate registration from bootstrapping through distinct register() and boot() methods
  4. Lazy Loading - Services are only instantiated when needed, not at registration time
  5. 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:

  1. Request Reception - The kernel receives the incoming request
  2. Middleware Pipeline - Request passes through global and route-specific middleware
  3. Route Resolution - The router matches the request to a route
  4. Controller Execution - The controller method is executed
  5. Response Generation - Response is created and sent back through middleware
  6. 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 responsibilities

2. 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.