Facades
Overview
Facades provide a static interface to classes available in the application's service container. They act as "static proxies" to underlying classes in the container, offering a concise, expressive syntax while maintaining testability and flexibility.
In Orchestr (like Laravel), facades serve as a convenient way to access services without manually resolving them from the container every time. Instead of repeatedly calling app.make('router'), you can simply use Route.get().
Concise Syntax
Clean, readable code without verbose dependency resolution
Service Container Integration
Automatic resolution of dependencies
Testability
Easy to mock and swap implementations
IDE Support
Full TypeScript type hints and autocomplete
Lazy Loading
Services are resolved only when actually used
How Facades Work
Behind every facade is a service registered in the application's container. When you call a static method on a facade, the following happens:
- Accessor Resolution: The facade identifies its accessor name (e.g., 'router' or 'db')
- Container Lookup: The facade resolves the service from the container using
app.make(accessor) - Instance Caching: The resolved instance is cached for subsequent calls
- Method Proxying: The static method call is forwarded to the actual instance method
// When you write this:
Route.get('/users', handler);
// This happens behind the scenes:
const router = app.make('router'); // Resolve Router from container
router.get('/users', handler); // Call the actual methodThe Facade Base Class
All facades extend the Facade base class, which provides the core functionality:
// Simplified implementation from /Users/samstreet/Projects/orchestr/src/Support/Facade.ts
export abstract class Facade {
private static app: Application;
private static resolvedInstances: Map<string, any> = new Map();
// Set the application instance (done during bootstrap)
static setFacadeApplication(app: Application): void {
Facade.app = app;
}
// Override this in child classes to specify the service key
protected static getFacadeAccessor(): string {
throw new Error('Facade does not implement getFacadeAccessor method.');
}
// Resolve the actual service from the container
protected static resolveFacadeInstance(name: string): any {
if (Facade.resolvedInstances.has(name)) {
return Facade.resolvedInstances.get(name);
}
if (Facade.app) {
const instance = Facade.app.make(name);
Facade.resolvedInstances.set(name, instance);
return instance;
}
throw new Error('A facade root has not been set.');
}
// Get the resolved instance
protected static getFacadeRoot(): any {
return Facade.resolveFacadeInstance(this.getFacadeAccessor());
}
}Built-in Facades
Orchestr provides two core facades out of the box.
Route Facade
The Route facade provides static access to the Router service for defining application routes.
Service Accessor: 'router'
Underlying Class: Router (from /Users/samstreet/Projects/orchestr/src/Routing/Router.ts)
Registration: Automatically registered by RouteServiceProvider
Available Methods
// HTTP verb methods
Route.get(uri: string, action: RouteAction): Route
Route.post(uri: string, action: RouteAction): Route
Route.put(uri: string, action: RouteAction): Route
Route.patch(uri: string, action: RouteAction): Route
Route.delete(uri: string, action: RouteAction): Route
Route.any(uri: string, action: RouteAction): Route
Route.match(methods: HttpMethod[], uri: string, action: RouteAction): Route
// Route grouping
Route.group(attributes: RouteGroupAttributes, callback: () => void): voidUsage Examples
import { Route } from '@orchestr-sh/orchestr';
// Simple GET route
Route.get('/users', async (req, res) => {
return res.json({ users: [] });
});
// Route with parameters
Route.get('/users/:id', async (req, res) => {
const id = req.routeParam('id');
return res.json({ user: { id } });
});
// POST route
Route.post('/users', async (req, res) => {
const data = req.only(['name', 'email']);
return res.status(201).json({ user: data });
});
// Route groups with shared attributes
Route.group({ prefix: '/api/v1', middleware: ['auth'] }, () => {
Route.get('/profile', profileHandler);
Route.post('/logout', logoutHandler);
});
// Multiple HTTP methods
Route.match(['GET', 'POST'], '/contact', contactHandler);DB Facade
The DB facade provides static access to the DatabaseManager service for database operations.
Service Accessor: 'db'
Underlying Class: DatabaseManager (from /Users/samstreet/Projects/orchestr/src/Database/DatabaseManager.ts)
Registration: Automatically registered by DatabaseServiceProvider
Available Methods
// Connection management
DB.connection(name?: string): Connection
// Query builder (via default connection)
DB.table(tableName: string): Builder
// Raw queries (via default connection)
DB.select(sql: string, bindings?: any[]): Promise<any[]>
DB.insert(sql: string, bindings?: any[]): Promise<any>
DB.update(sql: string, bindings?: any[]): Promise<number>
DB.delete(sql: string, bindings?: any[]): Promise<number>
DB.query(sql: string, bindings?: any[]): Promise<any>
// Transactions (via default connection)
DB.beginTransaction(): Promise<void>
DB.commit(): Promise<void>
DB.rollback(): Promise<void>
// Connection lifecycle
DB.disconnect(name?: string): Promise<void>
DB.disconnectAll(): Promise<void>Usage Examples
import { DB } from '@orchestr-sh/orchestr';
// Query builder
const users = await DB.table('users')
.where('active', true)
.orderBy('created_at', 'desc')
.get();
// Insert data
await DB.table('users').insert({
name: 'John Doe',
email: 'john@example.com',
created_at: new Date()
});
// Update records
await DB.table('users')
.where('id', userId)
.update({ last_login: new Date() });
// Delete records
await DB.table('users')
.where('deleted_at', '<', thirtyDaysAgo)
.delete();
// Raw queries
const results = await DB.select(
'SELECT * FROM users WHERE email = ?',
['john@example.com']
);
// Transactions
await DB.beginTransaction();
try {
await DB.table('accounts').where('id', 1).decrement('balance', 100);
await DB.table('accounts').where('id', 2).increment('balance', 100);
await DB.commit();
} catch (error) {
await DB.rollback();
throw error;
}
// Use a specific connection
const analyticsData = await DB.connection('analytics')
.table('events')
.count();Creating Custom Facades
You can create your own facades to provide static access to your services.
Step 1: Create the Service
First, create the service you want to facade:
// services/CacheService.ts
export class CacheService {
private cache: Map<string, any> = new Map();
get(key: string): any {
return this.cache.get(key);
}
set(key: string, value: any): void {
this.cache.set(key, value);
}
has(key: string): boolean {
return this.cache.has(key);
}
forget(key: string): void {
this.cache.delete(key);
}
flush(): void {
this.cache.clear();
}
}Step 2: Register the Service
Register your service in a service provider:
// providers/CacheServiceProvider.ts
import { ServiceProvider } from '@orchestr-sh/orchestr';
import { CacheService } from '../services/CacheService';
export class CacheServiceProvider extends ServiceProvider {
register(): void {
// Register as singleton so the same instance is used
this.app.singleton('cache', () => new CacheService());
}
}Step 3: Create the Facade
Create a facade class that extends Facade:
// facades/Cache.ts
import { Facade } from '@orchestr-sh/orchestr';
import { CacheService } from '../services/CacheService';
class CacheFacade extends Facade {
protected static getFacadeAccessor(): string {
return 'cache'; // Must match the container binding
}
// Type hints for IDE support
static get: CacheService['get'];
static set: CacheService['set'];
static has: CacheService['has'];
static forget: CacheService['forget'];
static flush: CacheService['flush'];
}
// Create proxy to enable static method calls
export const Cache = new Proxy(CacheFacade, {
get(target, prop) {
// Try to get from the facade root (the actual service)
try {
const root = (target as any).getFacadeRoot();
if (root && prop in root) {
const value = root[prop];
if (typeof value === 'function') {
return (...args: any[]) => value.apply(root, args);
}
return value;
}
} catch (error) {
// Facade root not available yet
}
// Fall back to static properties/methods
if (typeof prop === 'string' && prop in target) {
const value = (target as any)[prop];
if (typeof value === 'function') {
return value.bind(target);
}
return value;
}
return undefined;
}
}) as unknown as typeof CacheFacade & CacheService;Step 4: Bootstrap
Register your provider and set up facades:
import 'reflect-metadata';
import { Application, Facade } from '@orchestr-sh/orchestr';
import { CacheServiceProvider } from './providers/CacheServiceProvider';
import { Cache } from './facades/Cache';
// Create and bootstrap application
const app = new Application(__dirname);
// Set facade application
Facade.setFacadeApplication(app);
// Register providers
app.register(CacheServiceProvider);
await app.boot();
// Now use your facade!
Cache.set('user:1', { name: 'John Doe' });
const user = Cache.get('user:1');
console.log(user); // { name: 'John Doe' }Facade Accessor Implementation
The facade accessor pattern is the heart of how facades work. Here's a detailed look at the implementation:
Basic Accessor Pattern
class MyServiceFacade extends Facade {
// This tells the facade which container binding to resolve
protected static getFacadeAccessor(): string {
return 'my-service';
}
}Proxy Pattern for Method Forwarding
The proxy pattern enables calling instance methods statically:
export const MyService = new Proxy(MyServiceFacade, {
get(target, prop) {
// 1. Try to resolve the actual service instance
const root = (target as any).getFacadeRoot();
// 2. If the property is a method on the service, proxy it
if (root && typeof root[prop] === 'function') {
return (...args: any[]) => root[prop](...args);
}
// 3. If it's a property, return it directly
if (root && prop in root) {
return root[prop];
}
// 4. Fall back to static facade methods
if (typeof prop === 'string' && prop in target) {
const value = (target as any)[prop];
if (typeof value === 'function') {
return value.bind(target);
}
return value;
}
}
});Type Safety with TypeScript
Add type hints to get full IDE support:
class MyServiceFacade extends Facade {
protected static getFacadeAccessor(): string {
return 'my-service';
}
// Declare method signatures for autocomplete
static doSomething: MyService['doSomething'];
static calculate: MyService['calculate'];
static property: MyService['property'];
}
// Type the proxy export
export const MyService = new Proxy(MyServiceFacade, {
// ... proxy implementation
}) as unknown as typeof MyServiceFacade & MyService;Testing with Facades
Facades are designed to be testable despite their static interface.
Mock a Facade
You can swap the underlying implementation in tests:
import { describe, it, beforeEach, afterEach } from 'node:test';
import { Application, Facade } from '@orchestr-sh/orchestr';
import { Cache } from '../facades/Cache';
describe('User Service', () => {
let app: Application;
let mockCache: any;
beforeEach(async () => {
// Create fresh application
app = new Application(__dirname);
Facade.setFacadeApplication(app);
// Create mock cache
mockCache = {
get: (key: string) => 'mocked-value',
set: (key: string, value: any) => {},
has: (key: string) => true,
};
// Bind mock to container
app.instance('cache', mockCache);
await app.boot();
});
afterEach(() => {
// Clear resolved instances
Facade.clearResolvedInstances();
});
it('should use mocked cache', () => {
const value = Cache.get('test-key');
expect(value).toBe('mocked-value');
});
});Clear Resolved Instances
Between tests, clear the facade cache:
import { Facade } from '@orchestr-sh/orchestr';
afterEach(() => {
// Clear all cached facade instances
Facade.clearResolvedInstances();
// Or clear a specific facade
Facade.clearResolvedInstance('cache');
});Spy on Facade Methods
Since facades proxy to real instances, you can spy on the underlying service:
import { describe, it, mock } from 'node:test';
import { DB } from '@orchestr-sh/orchestr';
it('should call database insert', async () => {
// Get the underlying database manager
const dbManager = (DB as any).getFacadeRoot();
const connection = dbManager.connection();
// Spy on the table method
const tableSpy = mock.method(connection, 'table');
// Perform action that uses DB
await DB.table('users').insert({ name: 'Test' });
// Verify the spy was called
expect(tableSpy.mock.calls.length).toBe(1);
expect(tableSpy.mock.calls[0].arguments[0]).toBe('users');
});Facade vs Dependency Injection
Both facades and dependency injection (DI) have their place. Understanding when to use each is important.
Use Facades When:
- Writing application code (controllers, route handlers, scripts)
- You need concise syntax for frequently used services
- The dependency is obvious from context (routes always use Router)
- Rapid development is the priority
- Static helpers make the code more readable
// Good use of facades - application code
Route.get('/users', async (req, res) => {
const users = await DB.table('users')
.where('active', true)
.get();
return res.json({ users });
});Use Dependency Injection When:
- Writing reusable packages or libraries
- Complex business logic with multiple dependencies
- Testing is critical and you need explicit dependencies
- Dependencies aren't obvious from the class context
- You want loose coupling between classes
// Good use of DI - service layer
import { Injectable } from '@orchestr-sh/orchestr';
@Injectable()
export class UserService {
constructor(
private emailService: EmailService,
private notificationService: NotificationService,
private logger: Logger
) {}
async createUser(data: CreateUserDto) {
// Complex business logic with explicit dependencies
const user = await this.saveUser(data);
await this.emailService.sendWelcome(user);
await this.notificationService.notify(user);
this.logger.info('User created', { userId: user.id });
return user;
}
}Comparison Example
Here's the same functionality implemented both ways:
// Using Facades (concise, good for simple cases)
Route.post('/users', async (req, res) => {
const data = req.only(['name', 'email']);
await DB.beginTransaction();
try {
const user = await DB.table('users').insert(data);
await DB.table('audit_log').insert({
action: 'user_created',
user_id: user.id
});
await DB.commit();
return res.json({ user });
} catch (error) {
await DB.rollback();
throw error;
}
});
// Using Dependency Injection (testable, good for complex logic)
@Injectable()
export class UserController extends Controller {
constructor(
private userService: UserService,
private auditLogger: AuditLogger
) {
super();
}
async store(req: Request, res: Response) {
const data = req.only(['name', 'email']);
const user = await this.userService.createWithAudit(data);
return res.json({ user });
}
}
// Service with injected dependencies
@Injectable()
export class UserService {
constructor(
private db: DatabaseManager,
private auditLogger: AuditLogger
) {}
async createWithAudit(data: CreateUserDto) {
await this.db.beginTransaction();
try {
const user = await this.db.table('users').insert(data);
await this.auditLogger.log('user_created', user.id);
await this.db.commit();
return user;
} catch (error) {
await this.db.rollback();
throw error;
}
}
}Hybrid Approach
You can combine both patterns effectively:
@Injectable()
export class OrderService {
// Inject complex dependencies
constructor(
private paymentGateway: PaymentGateway,
private inventoryService: InventoryService
) {}
async createOrder(data: CreateOrderDto) {
// Use DI for injected services
const hasStock = await this.inventoryService.checkStock(data.items);
if (!hasStock) {
throw new Error('Insufficient stock');
}
// Use facades for framework services (concise and appropriate)
await DB.beginTransaction();
try {
const order = await DB.table('orders').insert(data);
await this.paymentGateway.charge(order);
await this.inventoryService.reserve(data.items);
await DB.commit();
return order;
} catch (error) {
await DB.rollback();
throw error;
}
}
}Best Practices
1. Use Facades for Framework Services
Facades work best for core framework services like routing and database:
// Good
Route.get('/users', handler);
const users = await DB.table('users').get();
// Overkill
constructor(private router: Router, private db: DatabaseManager) {}2. Don't Create Too Many Facades
Only create facades for frequently used services. Not every service needs a facade.
// Good - frequently used
Cache.get('key');
Log.info('message');
// Questionable - rarely used
PDF.generate(data);
Markdown.parse(text);3. Always Register Services as Singletons
Facades assume the underlying service is a singleton:
// Good
app.singleton('cache', () => new CacheService());
// Bad - will create new instances on each call
app.bind('cache', () => new CacheService());4. Provide Type Hints
Always add type hints to your facade for IDE support:
class MyFacade extends Facade {
protected static getFacadeAccessor(): string {
return 'my-service';
}
// Add type hints
static method1: MyService['method1'];
static method2: MyService['method2'];
}5. Clear Instances in Tests
Always clear facade instances between tests to avoid state leakage:
afterEach(() => {
Facade.clearResolvedInstances();
});6. Document Your Facades
Add JSDoc comments to help other developers:
/**
* Cache Facade
*
* Provides static access to the caching service.
*
* @example
* ```typescript
* Cache.set('user:1', userData);
* const user = Cache.get('user:1');
* ```
*/
export const Cache = new Proxy(CacheFacade, {
// ...
});Important Setup
7. Bootstrap Properly
Always set the facade application before using facades:
import 'reflect-metadata';
import { Application, Facade } from '@orchestr-sh/orchestr';
const app = new Application(__dirname);
// IMPORTANT: Set this before using any facades
Facade.setFacadeApplication(app);
// Register providers
app.register(RouteServiceProvider);
await app.boot();
// Now facades will work
Route.get('/test', handler);8. Consider the Trade-offs
Facades trade explicit dependencies for convenience. Use them wisely:
Pros:
- Concise, readable code
- Less boilerplate
- Familiar to Laravel developers
- Easy to use in simple scenarios
Cons:
- Hidden dependencies (not visible in constructor)
- Harder to test in isolation without mocking
- Can make code harder to understand for new developers
- Static nature limits some advanced patterns
Summary
Facades provide an elegant, expressive way to access services from the container through a static interface. They're perfect for application code, route definitions, and quick database queries. However, for complex business logic, reusable packages, or when testing is paramount, traditional dependency injection may be more appropriate.
Key takeaways:
- Facades are static proxies to container services
- They resolve services lazily and cache instances
- Built-in facades include
RouteandDB - Create custom facades by extending
Facadeand using proxies - Facades are testable through container mocking
- Use facades for framework services, DI for business logic
- Always bootstrap facades properly with
Facade.setFacadeApplication(app)