Skip to main content
Version: 🚧 Canary

🎭 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 live under the primitives namespace. They enhance handlers with additional functionality while maintaining the same interface — decorator-style, composable with compose.

import { primitives } from '@nodeblocks/backend-sdk';

const {
withLogging,
withPagination,
withPaginatedProperty,
DEFAULT_REDACTION,
DEFAULT_SANITIZATION,
} = primitives;

Main exports: withLogging, withPagination, withPaginatedProperty, WithLoggingOptions, RedactOption, DEFAULT_REDACTION, DEFAULT_SANITIZATION, PaginationParams, PaginationResult.

Top-level import { withLogging } from '@nodeblocks/backend-sdk' is not supported — use primitives.

Key Features

  • Non-Intrusive: Add functionality without changing handler signatures
  • Composable: Combine multiple wrappers on the same handler
  • Configurable: withLogging accepts options; pagination wrappers read query params
  • Type Safe: Full TypeScript support with exported interfaces

📝 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 logging
  • options?: 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 { primitives } from '@nodeblocks/backend-sdk';

const { withLogging } = primitives;

// Simple logging with default settings (level: 'info', logger: nodeblocksLogger)
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]' },
],
});

Defaults: level: 'info', logger: nodeblocksLogger (from utils.nodeblocksLogger).

Logger injection: When a wrapped function receives a payload object with context, withLogging automatically sets payload.logger to the configured logger before calling the inner function.

Log Levels

Pass the level via the options object (there is no string shorthand overload):

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging } = primitives;

withLogging(handler, { level: 'trace' }); // Most detailed
withLogging(handler, { level: 'debug' }); // Development info
withLogging(handler, { level: 'info' }); // General information (default)
withLogging(handler, { level: 'warn' }); // Warnings
withLogging(handler, { level: 'error' }); // Errors
withLogging(handler, { level: '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 via DEFAULT_SANITIZATION (infrastructure fields on RouteHandlerPayload) and DEFAULT_REDACTION (field/pattern rules).

Infrastructure Fields (DEFAULT_SANITIZATION — applied to payload objects):

  • Database connections → 🗄️ [Database]
  • Config objects → ⚙️ [Configuration]
  • File storage drivers → 📂 [FileStorageDriver]
  • OAuth drivers → 🔐 [GoogleOAuthDriver], 🔐 [LineOAuthDriver], 🔐 [TwitterOAuthDriver]
  • Mail services → ✉️ [MailService]
  • Request/Response → 📥 [Request] / 📤 [Response]
  • Logger instances → 📝 [Logger]

Sensitive Data Patterns (DEFAULT_REDACTION — field and pattern rules):

  • Passwords: password, pass, pwd, pw[REDACTED_PASSWORD]
  • Email addresses → [REDACTED_EMAIL]
  • Credit cards → [REDACTED_CREDIT_CARD]
  • Tokens and authorization → [REDACTED_TOKEN], [REDACTED_AUTHORIZATION]
  • Secrets, credentials, API keys → [REDACTED_SECRET], [REDACTED_CREDENTIAL], [REDACTED_API_KEY]
  • Phone numbers → [REDACTED_PHONE]
  • Address, private, sensitive, signature, SSN fields
  • Bearer tokens in string values → [REDACTED_BEARER_TOKEN]

Custom Redaction

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, DEFAULT_REDACTION } = primitives;

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 is a Record — use Object.values)
...Object.values(DEFAULT_REDACTION),
],
});

📄 Automatic Pagination

withPagination

Proxies context.db.*.find() so that pagination is applied automatically. Reads page and limit from requestQuery, strips them from downstream query filters, and applies skip/limit to MongoDB find options.

When the handler calls cursor.toArray(), the return value is not a plain array — it is:

{ data: T[]; metadata: PaginationResult }

Handlers typically store this on context.data (e.g. { products: { data, metadata } }). Terminators must handle both plain arrays and the paginated shape.

import { primitives } from '@nodeblocks/backend-sdk';

const { withPagination } = primitives;

const findProducts = async (payload: RouteHandlerPayload) => {
const products = await payload.context.db.products
.find(payload.params.requestQuery || {})
.toArray();
// When wrapped with withPagination, `products` is { data, metadata }
return ok(mergeData(payload, { products }));
};

const getPaginatedProducts = withPagination(findProducts);

Pagination Parameters

ParameterTypeDefaultDescription
pagenumber1Page number (1-based indexing)
limitnumber10Number of items per page

page and limit are removed from params.requestQuery passed to the inner handler so they are not treated as filter fields.

Response Structure

Pagination metadata is returned from toArray() as metadata alongside data. A terminator formats the HTTP response — the SDK's normalizeProductsListTerminator (in product handlers) handles both plain arrays and { data, metadata }:

// Simplified from SDK product list terminator
export const normalizeProductsListTerminator = (
result: Result<RouteHandlerPayload, Error>
) => {
if (result.isErr()) throw result.error;
const { context } = result.value;
const products = context.data.products;

if (Array.isArray(products)) {
return products.map(formatProduct);
}

return {
data: products.data.map(formatProduct),
metadata: { pagination: products.metadata },
};
};

Usage Example

// Request: GET /api/products?page=2&limit=20

// Response shape (after terminator):
{
"data": [
{ "id": "prod-21", "name": "Product 21" },
{ "id": "prod-22", "name": "Product 22" }
],
"metadata": {
"pagination": {
"page": 2,
"limit": 20,
"total": 150,
"totalPages": 8,
"hasNext": true,
"hasPrev": true
}
}
}

withPaginatedProperty

Paginates a nested array on a document returned by findOne() (e.g. organization members). Proxies context.db.*.findOne() and replaces the array at propertyPath with { data, metadata }.

import { primitives } from '@nodeblocks/backend-sdk';

const { withPaginatedProperty, withLogging } = primitives;

// Paginate organization.members from a findOne result
const paginatedMembersHandler = withPaginatedProperty(
withLogging(findOrganizationMembers, { level: 'info' }),
['members']
);

Used in SDK routes such as GET /organizations/:organizationId/members.

withPagination does not accept options — it reads page and limit from requestQuery and computes skip internally.


🔧 Advanced Usage

The examples below are illustrative patterns — symbols like withRoute, ok, and mergeData come from other SDK modules.

Combining Wrappers

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, withPagination } = primitives;

// Add both logging and pagination
const enhancedHandler = withLogging(withPagination(getAllProducts), {
level: 'debug',
});

// Or compose them in any order
const alternativeHandler = withPagination(
withLogging(getAllProducts, { level: 'info' })
);

Conditional Wrapping

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, withPagination } = primitives;

const createHandler = (config: Config) => {
let handler = getAllProducts;

if (config.enablePagination) {
handler = withPagination(handler);
}

if (config.environment === 'development') {
handler = withLogging(handler, { level: 'debug' });
} else if (config.environment === 'production') {
handler = withLogging(handler, { level: 'info' });
}

return handler;
};

Custom Logger Integration (example pattern)

WithLoggingOptions.logger expects a Pino-compatible Logger. Third-party loggers may not work unless they implement the same interface.

import { primitives, utils } from '@nodeblocks/backend-sdk';

const { withLogging } = primitives;
const { nodeblocksLogger } = utils;

// Use SDK logger with a child binding
const customLogger = nodeblocksLogger.child({ service: 'user-service' });

const loggedHandler = withLogging(createUserHandler, {
logger: customLogger,
level: 'debug',
});

📝 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, { level: '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 — toArray() returns { data, metadata } when wrapped
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, compose, lift } = primitives;

const getProductsHandler = compose(
withPagination(
withLogging(findProducts, { level: 'debug' })
),
lift(normalizeProductsListTerminator)
);

// Pipeline:
// 1. withPagination proxies db.find and paginates toArray()
// 2. withLogging logs args/return with redaction
// 3. normalizeProductsListTerminator formats { data, metadata } for HTTP response

📐 Best Practices

1. Use Appropriate Log Levels

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, compose } = primitives;

// ✅ Good: Use appropriate log levels for different operations
const userPipeline = compose(
withLogging(validateUser, { level: 'debug' }),
withLogging(saveUser, { level: 'info' }),
withLogging(formatResponse, { level: 'trace' })
);

// ❌ Avoid: Using the same level for everything
const badPipeline = compose(
withLogging(validateUser, { level: 'info' }),
withLogging(saveUser, { level: 'info' }),
withLogging(formatResponse, { level: '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

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, compose } = primitives;

// ✅ Good: Log at appropriate levels
const userPipeline = compose(
withLogging(validateUser, { level: 'debug' }),
withLogging(saveUser, { level: 'info' }),
withLogging(formatResponse, { level: 'trace' })
);

// ✅ Good: Conditional logging
const getLoggedHandler = (isDevelopment: boolean) => {
const level = isDevelopment ? 'debug' : 'info';
return withLogging(handler, { level });
};

4. Compose Wrappers with Business Logic

import { primitives } from '@nodeblocks/backend-sdk';

const { withLogging, withPagination, compose } = primitives;

// ✅ Good: Separate concerns clearly
const businessLogic = compose(
validateInput,
processData,
formatResponse
);

const enhancedHandler = withLogging(withPagination(businessLogic), {
level: 'debug',
});

🔗 See Also