Skip to main content
Version: 0.15.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 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 — withRoute and mergeData come from primitives and handlers; ok comes from neverthrow. Other business helpers are application-defined.

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.

In SDK 0.15.0, that type comes from Pino 10. Upgrade an application-supplied Pino logger to pino@^10, or use the SDK logger and its children as in the example below. See custom logger configuration for migration and runtime requirements.

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, compose, lift, orThrow, withRoute } = 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: compose(loggedCreateUser, lift(orThrow([], [['context', 'data', 'user']]))),
});

Paginated Product Listing​

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

const { withPagination, compose, lift, orThrow, withRoute } = 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: compose(getPaginatedProducts, lift(orThrow([], [['context', 'data', 'products']]))),
});

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​