Skip to main content
Version: 0.6.0 (Latest)

🎭 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:

ParameterTypeDefaultDescription
pagenumber1Page number (1-based indexing)
limitnumber10Number 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