Skip to main content

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();