🎭 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 flexible parameter ordering.
import { primitives } from '@nodeblocks/backend-sdk';
const { withLogging } = primitives;
// Simple logging with default settings
const loggedHandler = withLogging(createUserHandler);
// Specify log level
const debugHandler = withLogging(createUserHandler, 'debug');
// Specify custom logger
const customLoggedHandler = withLogging(createUserHandler, customLogger);
// Specify both logger and level (any order)
const fullLoggedHandler = withLogging(createUserHandler, customLogger, 'trace');
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
- Sanitized result/return value
- Promise detection for async functions
{
"args": [{"params": {"requestBody": {"name": "John"}}}],
"functionName": "createUserHandler",
"msg": "Function: createUserHandler",
"result": {"user": {"id": "123", "name": "John"}}
}
Smart Sanitization
The withLogging
function automatically sanitizes sensitive objects:
- Database connections →
🗄️ [Database]
- Config objects →
⚙️ [Config]
- Mail services →
📧 [MailService]
- Request/Response →
📤 [Request]
/📥 [Response]
- Logger instances →
📝 [Logger]
📄 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