Advanced Patterns
Advanced usage patterns and best practices for the Core package.
Shared Services Pattern
Create shared services that can be used across handlers:
// src/shared/database.ts
import { Pool } from 'pg';
let pool: Pool | null = null;
export function getDatabase(): Pool {
if (!pool) {
pool = new Pool({
connectionString: process.env.DATABASE_URL,
});
}
return pool;
}
// src/modules/user/handlers/list/index.ts
import { APIGatewayProxyHandler } from 'aws-lambda';
import { getDatabase } from '../../../../shared/database';
export const handler: APIGatewayProxyHandler = async () => {
const db = getDatabase();
const result = await db.query('SELECT * FROM users');
return {
statusCode: 200,
body: JSON.stringify(result.rows),
};
};
Middleware Pattern
Create reusable middleware functions:
// src/shared/middleware.ts
import { APIGatewayProxyEvent, APIGatewayProxyResult } from 'aws-lambda';
type Handler = (
event: APIGatewayProxyEvent
) => Promise<APIGatewayProxyResult>;
type Middleware = (handler: Handler) => Handler;
// Authentication middleware
export const withAuth: Middleware = (handler) => async (event) => {
const token = event.headers['Authorization'];
if (!token) {
return {
statusCode: 401,
body: JSON.stringify({ error: 'Unauthorized' }),
};
}
// Validate token...
return handler(event);
};
// Logging middleware
export const withLogging: Middleware = (handler) => async (event) => {
console.log(`[${event.httpMethod}] ${event.path}`);
const start = Date.now();
const result = await handler(event);
console.log(`Response: ${result.statusCode} (${Date.now() - start}ms)`);
return result;
};
// Compose middlewares
export const compose = (...middlewares: Middleware[]) =>
(handler: Handler): Handler =>
middlewares.reduceRight((h, m) => m(h), handler);
Usage:
// src/modules/user/handlers/create/index.ts
import { compose, withAuth, withLogging } from '../../../../shared/middleware';
const createUser = async (event) => {
// Handler logic...
return { statusCode: 201, body: '{}' };
};
export const handler = compose(
withLogging,
withAuth
)(createUser);
Error Handling Pattern
Centralized error handling:
// src/shared/errors.ts
export class AppError extends Error {
constructor(
message: string,
public statusCode: number = 500,
public code?: string
) {
super(message);
this.name = 'AppError';
}
}
export class ValidationError extends AppError {
constructor(message: string) {
super(message, 400, 'VALIDATION_ERROR');
}
}
export class NotFoundError extends AppError {
constructor(resource: string) {
super(`${resource} not found`, 404, 'NOT_FOUND');
}
}
export class UnauthorizedError extends AppError {
constructor() {
super('Unauthorized', 401, 'UNAUTHORIZED');
}
}
// src/shared/handler.ts
import { APIGatewayProxyHandler, APIGatewayProxyResult } from 'aws-lambda';
import { AppError } from './errors';
type AsyncHandler = (event: any) => Promise<any>;
export function createHandler(fn: AsyncHandler): APIGatewayProxyHandler {
return async (event, context) => {
try {
const result = await fn(event);
return {
statusCode: 200,
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(result),
};
} catch (error) {
console.error('Handler error:', error);
if (error instanceof AppError) {
return {
statusCode: error.statusCode,
body: JSON.stringify({
error: error.message,
code: error.code,
}),
};
}
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal server error' }),
};
}
};
}
Usage:
// src/modules/user/handlers/get/index.ts
import { createHandler } from '../../../../shared/handler';
import { NotFoundError } from '../../../../shared/errors';
export const handler = createHandler(async (event) => {
const { id } = event.pathParameters;
const user = await findUser(id);
if (!user) {
throw new NotFoundError('User');
}
return user;
});
Repository Pattern
Abstract data access:
// src/modules/user/repository.ts
interface User {
id: string;
name: string;
email: string;
}
interface UserRepository {
findAll(): Promise<User[]>;
findById(id: string): Promise<User | null>;
create(data: Omit<User, 'id'>): Promise<User>;
update(id: string, data: Partial<User>): Promise<User>;
delete(id: string): Promise<void>;
}
// In-memory implementation
export class InMemoryUserRepository implements UserRepository {
private users = new Map<string, User>();
async findAll(): Promise<User[]> {
return Array.from(this.users.values());
}
async findById(id: string): Promise<User | null> {
return this.users.get(id) || null;
}
async create(data: Omit<User, 'id'>): Promise<User> {
const user = { id: crypto.randomUUID(), ...data };
this.users.set(user.id, user);
return user;
}
async update(id: string, data: Partial<User>): Promise<User> {
const user = this.users.get(id);
if (!user) throw new Error('User not found');
const updated = { ...user, ...data };
this.users.set(id, updated);
return updated;
}
async delete(id: string): Promise<void> {
this.users.delete(id);
}
}
// Singleton instance
export const userRepository = new InMemoryUserRepository();
Event-Driven Pattern
Combine HTTP and SQS for event-driven architecture:
# functions.yml
createOrder:
handler: handlers/create/index.handler
events:
- http:
path: orders
method: POST
processOrderPayment:
handler: handlers/payment/index.handler
events:
- sqs:
arn:
Fn::GetAtt: [OrderPaymentQueue, Arn]
sendOrderConfirmation:
handler: handlers/confirmation/index.handler
events:
- sqs:
arn:
Fn::GetAtt: [OrderConfirmationQueue, Arn]
// handlers/create/index.ts
import { MonolithServer } from '@serverless-monolith/core';
export const handler = createHandler(async (event) => {
const order = await createOrder(JSON.parse(event.body));
// Trigger payment processing
const server = MonolithServer.getInstance();
await server.sendSqsMessage('OrderPaymentQueue', {
orderId: order.id,
amount: order.total,
});
return order;
});
Custom Log Storage
Store logs in a database:
// src/adapters/PostgresLogAdapter.ts
import { ILogStorageAdapter, ExecutionLog } from '@serverless-monolith/core';
import { Pool } from 'pg';
export class PostgresLogAdapter implements ILogStorageAdapter {
constructor(private pool: Pool) {}
async save(log: ExecutionLog): Promise<void> {
await this.pool.query(
`INSERT INTO execution_logs (execution_id, type, data, created_at)
VALUES ($1, $2, $3, NOW())`,
[log.executionId, log.type, JSON.stringify(log)]
);
}
async findByExecutionId(id: string): Promise<ExecutionLog | null> {
const result = await this.pool.query(
'SELECT data FROM execution_logs WHERE execution_id = $1',
[id]
);
return result.rows[0]?.data || null;
}
async findAll(): Promise<ExecutionLog[]> {
const result = await this.pool.query(
'SELECT data FROM execution_logs ORDER BY created_at DESC LIMIT 100'
);
return result.rows.map(r => r.data);
}
}
Usage:
const server = MonolithServer.create('./config.ts');
server.setLogStorageAdapter(new PostgresLogAdapter(pool));
await server.start();