🎭 Handler Utilities
Handler wrappers provide cross-cutting concerns that can be applied to any handler function. These utilities add functionality like logging, pagination, and other middleware-like features without changing the core business logic of your handlers.
🎯 Overview
Handler wrappers are utilities that enhance handlers with additional functionality while maintaining the same interface. They follow the decorator pattern and can be composed together to add multiple layers of functionality.
Key Features
- Non-Intrusive: Add functionality without changing handler signatures
- Composable: Combine multiple wrappers on the same handler
- Configurable: Flexible configuration options for each wrapper
- Type Safe: Full TypeScript support with proper typing
📝 Function Logging
withLogging
Wraps any function to provide comprehensive logging capabilities with automatic redaction of sensitive data.
Parameters:
fn: (...args: T[]) => R— The function to wrap with loggingoptions?: WithLoggingOptions— Configuration options for logging behavior
WithLoggingOptions:
interface WithLoggingOptions {
logger?: Logger; // Custom logger instance (default: nodeblocksLogger)
level?: 'info' | 'debug' | 'warn' | 'error' | 'fatal' | 'trace'; // Log level
redact?: RegExp | RedactOption[]; // Sensitive data redaction rules
}
RedactOption:
interface RedactOption {
approach: 'fields' | 'patterns'; // Redaction method
pattern: RegExp; // Pattern to match
replacement?: string; // Replacement text
}
import { withLogging } from '@nodeblocks/backend-sdk';
// Simple logging with default settings
const loggedHandler = withLogging(createUserHandler);
// Specify log level
const debugHandler = withLogging(createUserHandler, { level: 'debug' });
// Specify custom logger
const customLoggedHandler = withLogging(createUserHandler, { logger: customLogger });
// Specify both logger and level
const fullLoggedHandler = withLogging(createUserHandler, {
logger: customLogger,
level: 'trace'
});
// Advanced usage with redaction
const secureHandler = withLogging(createUserHandler, {
level: 'info',
redact: [
{ approach: 'fields', pattern: /^password$/i, replacement: '[REDACTED_PASSWORD]' },
{ approach: 'patterns', pattern: /secret/i, replacement: '[REDACTED]' }
]
});
Log Levels
// Available log levels
withLogging(handler, 'trace'); // Most detailed
withLogging(handler, 'debug'); // Development info
withLogging(handler, 'info'); // General information
withLogging(handler, 'warn'); // Warnings
withLogging(handler, 'error'); // Errors
withLogging(handler, 'fatal'); // Critical errors
Log Output
All log levels produce structured logs with:
- Function name
- Sanitized input arguments with automatic redaction
- Sanitized result/return value with automatic redaction
- Promise detection for async functions
- Result type detection for neverthrow Result objects
{
"args": [
{
"params": {
"requestBody": {
"name": "John",
"email": "[REDACTED_EMAIL]",
"password": "[REDACTED_PASSWORD]"
}
}
}
],
"functionName": "createUserHandler",
"return": {
"promise": false,
"result": { "ok": true },
"value": {
"user": {
"id": "123",
"name": "John",
"email": "[REDACTED_EMAIL]"
}
}
}
}
Automatic Redaction
The withLogging function automatically redacts sensitive data:
Infrastructure Fields (always redacted):
- Database connections →
🗄️ [Database] - Config objects →
⚙️ [Configuration] - File storage drivers →
📂 [FileStorageDriver] - OAuth drivers →
🔐 [GoogleOAuthDriver], etc. - Mail services →
✉️ [MailService] - Request/Response →
📥 [Request]/📤 [Response] - Logger instances →
📝 [Logger]
Sensitive Data Patterns (automatically redacted):
- Passwords:
password,pass,pwd,pw→[REDACTED_PASSWORD] - Email addresses →
[REDACTED_EMAIL] - Credit cards →
[REDACTED_CREDIT_CARD] - Tokens and authorization →
[REDACTED_TOKEN],[REDACTED_AUTHORIZATION] - Secrets →
[REDACTED_SECRET] - Phone numbers →
[REDACTED_PHONE]
Custom Redaction
import { withLogging, DEFAULT_REDACTION } from '@nodeblocks/backend-sdk';
const secureHandler = withLogging(myFunction, {
redact: [
// Add custom redaction rules
{ approach: 'fields', pattern: /apiKey/i, replacement: '[REDACTED_API_KEY]' },
{ approach: 'patterns', pattern: /customSecret/i, replacement: '[HIDDEN]' },
// Include default rules
...DEFAULT_REDACTION
]
});
📄 Automatic Pagination
The withPagination wrapper is implemented. It augments MongoDB-style find calls by applying page and limit from requestQuery, and attaches pagination metadata to the payload for downstream handlers/terminators.
withPagination
Adds automatic pagination to database-backed route handlers, especially useful for MongoDB collections.
import { primitives } from '@nodeblocks/backend-sdk';
const { withPagination } = primitives;
// Basic handler that returns all products
const getAllProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
};
// Add pagination to the handler
const getPaginatedProducts = withPagination(getAllProducts);
Pagination Parameters
The wrapper automatically extracts these parameters from the request query:
| Parameter | Type | Default | Description |
|---|---|---|---|
page | number | 1 | Page number (1-based indexing) |
limit | number | 10 | Number of items per page |
Response Structure
withPagination stores pagination metadata on the payload as payload.paginationResult. Use a terminator to include it in the HTTP response if desired.
// Example terminator that returns data + pagination
export const normalizeProductsListTerminator = (
result: Result<RouteHandlerPayload, NodeblocksError>
) => {
if (result.isErr()) throw result.error;
const payload = result.value;
const { data, pagination } = payload.paginationResult || {};
return { data: data ?? payload.context.data.products, pagination };
};
Usage Examples
// Request: GET /api/products?page=2&limit=20
// Current Response (without pagination metadata):
[
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
]
// Future Response (with pagination metadata):
{
"data": [
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
],
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
🔧 Advanced Usage
Combining Wrappers
You can combine multiple wrappers on the same handler:
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination } = primitives;
// Add both logging and pagination
const enhancedHandler = withLogging(
withPagination(getAllProducts),
'debug'
);
// Or compose them in any order
const alternativeHandler = withPagination(
withLogging(getAllProducts, 'info')
);
Conditional Wrapping
Apply wrappers conditionally based on environment or configuration:
const createHandler = (config: Config) => {
let handler = getAllProducts;
// Add pagination if enabled
if (config.enablePagination) {
handler = withPagination(handler);
}
// Add logging based on environment
if (config.environment === 'development') {
handler = withLogging(handler, 'debug');
} else if (config.environment === 'production') {
handler = withLogging(handler, 'info');
}
return handler;
};
Custom Logger Integration
import { createLogger } from 'winston';
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
// Create custom logger
const customLogger = createLogger({
level: 'debug',
format: winston.format.json(),
transports: [
new winston.transports.File({ filename: 'app.log' }),
new winston.transports.Console()
]
});
// Use custom logger with specific level
const loggedHandler = withLogging(
createUserHandler,
customLogger,
'debug'
);
Configuration
withPagination currently does not accept options. It reads page and limit from requestQuery and computes skip internally.
📝 Practical Examples
Logged User Creation
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
const createUserHandler = async (payload: RouteHandlerPayload) => {
const { name, email } = payload.params.requestBody;
// Business logic here
const user = await userService.create({ name, email });
return ok(mergeData(payload, { user }));
};
// Add comprehensive logging
const loggedCreateUser = withLogging(createUserHandler, 'debug');
// Usage in route
const createUserRoute = withRoute({
method: 'POST',
path: '/users',
handler: loggedCreateUser,
});
Paginated Product Listing
import { primitives } from '@nodeblocks/backend-sdk';
const { withPagination } = primitives;
const getAllProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products
.find({})
.sort({ createdAt: -1 })
.toArray();
return ok(mergeData(payload, { products }));
};
// Add pagination (currently returns array directly)
const getPaginatedProducts = withPagination(getAllProducts);
// Usage in route
const getProductsRoute = withRoute({
method: 'GET',
path: '/products',
handler: getPaginatedProducts,
});
Combined Logging and Pagination
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging, withPagination } = primitives;
const getProductsHandler = compose(
withLogging(withPagination(findProducts), 'debug'),
lift(normalizeProductsList)
);
// This creates a pipeline that:
// 1. Applies pagination to the database query
// 2. Logs the paginated results
// 3. Normalizes the product list format
📐 Best Practices
1. Use Appropriate Log Levels
// ✅ Good: Use appropriate log levels for different operations
const userPipeline = compose(
withLogging(validateUser, 'debug'), // Debug for validation
withLogging(saveUser, 'info'), // Info for database ops
withLogging(formatResponse, 'trace') // Trace for formatting
);
// ❌ Avoid: Using the same level for everything
const badPipeline = compose(
withLogging(validateUser, 'info'),
withLogging(saveUser, 'info'),
withLogging(formatResponse, 'info')
);
2. Apply Wrappers at the Right Level
// ✅ Good: Apply pagination to database operations
const getProducts = withPagination(
async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return ok(mergeData(payload, { products }));
}
);
// ❌ Avoid: Applying pagination to already processed data
const badGetProducts = async (payload) => {
const products = await payload.context.db.products.find({}).toArray();
return withPagination(() => ok(mergeData(payload, { products })));
};
3. Use Logging Strategically
// ✅ Good: Log at appropriate levels
const userPipeline = compose(
withLogging(validateUser, 'debug'), // Debug for validation
withLogging(saveUser, 'info'), // Info for database ops
withLogging(formatResponse, 'trace') // Trace for formatting
);
// ✅ Good: Conditional logging
const getLoggedHandler = (isDevelopment: boolean) => {
const logLevel = isDevelopment ? 'debug' : 'info';
return withLogging(handler, logLevel);
};
4. Compose Wrappers with Business Logic
// ✅ Good: Separate concerns clearly
const businessLogic = compose(
validateInput,
processData,
formatResponse
);
const enhancedHandler = withLogging(
withPagination(businessLogic),
'debug'
);
// ❌ Avoid: Mixing wrapper logic with business logic
const mixedHandler = async (payload) => {
// Logging logic mixed with business logic
console.log('Starting handler');
const result = await businessLogic(payload);
console.log('Handler completed');
return result;
};
🔗 See Also
- Composition Utilities - Function composition and error handling
- Handler Component - Basic handler concepts
- Route Component - Route definitions
- Error Handling - Result types and error patterns