Skip to main content
Version: 0.4.2 (Previous)

🔧 Composition Utilities

The Nodeblocks SDK provides essential utilities for composing handlers, handling asynchronous operations, and building complex business logic pipelines. These utilities enable you to build complex, error-safe pipelines by combining simple functions in predictable ways.


🎯 Overview

Composition utilities provide the building blocks for robust and maintainable business logic. They handle function composition and error propagation, allowing you to chain operations safely and efficiently.

Key Features

  • Function Composition: Combine multiple functions into single pipelines
  • Error-Safe Operations: Automatic error propagation and handling
  • Async Support: Seamless handling of asynchronous operations
  • Type Safety: Full TypeScript support with proper typing

🔗 Basic Composition

compose

Combines multiple functions into a single pipeline, executing them from right to left.

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

const { compose } = primitives;

const processUser = compose(
validateUser,
saveUser,
formatResponse
);

// Equivalent to: formatResponse(saveUser(validateUser(input)))

lift

Lifts a regular function into a payload context.

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

const { lift } = primitives;

const normalizeUser = (user: User) => {
const { _id, ...normalized } = user;
return normalized;
};

// Lift the function to work in composition
const handler = compose(
getUserFromDb,
lift(normalizeUser) // Now works with payload results
);

mergeData

Merges data into the payload context for use by subsequent handlers in a composition pipeline.

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

const { mergeData } = handlers;

const enrichUserData = async (payload: RouteHandlerPayload) => {
const profile = await profileService.getProfile(payload.context.data.userId);
return mergeData(payload, { profile });
};

const getUserHandler = compose(
fetchUserFromDb,
flatMapAsync(enrichUserData), // profile is now available in payload.context.data
lift(formatUserResponse)
);

Parameters:

  • payload: The current payload object
  • data: Object containing data to merge into payload.context.data

Returns: Updated payload object with merged data

Usage Examples:

// Merge single value
return mergeData(payload, { userId: '123' });
// Merge multiple values
return mergeData(payload, {
user: userData,
profile: profileData,
settings: settingsData
});

// Merge in async handlers
const createUser = async (payload: RouteHandlerPayload) => {
const user = await db.users.create(payload.params.requestBody);
return mergeData(payload, { user });
};

🔄 Error-Safe Composition

flatMap

Chains synchronous operations that return a Result type, enabling error-safe composition.

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

const { flatMap } = primitives;

const processUserData = compose(
validateUserInput,
flatMap(enrichUserData), // Only runs if validation succeeds
flatMap(formatUserData)
);

flatMapAsync

Chains asynchronous operations that return a Result type, enabling error-safe composition.

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

const { flatMapAsync } = primitives;

const createAndFetchUser = compose(
createUserInDb,
flatMapAsync(fetchUserById), // Only runs if createUserInDb succeeds
lift(normalizeUserResponse)
);

📝 Practical Examples

Multi-Step Data Processing

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

const { compose, flatMapAsync, lift } = primitives;

// Step 1: Enrich user data
const enrichUserData = async (payload: RouteHandlerPayload) => {
const profile = await profileService.getProfile(payload.context.data.userId);
return mergeData(payload, { profile });
};

// Step 2: Format data for response
const formatUserResponse = (payload: RouteHandlerPayload) => {
const { user, profile } = payload.context.data;
return { ...user, profile };
};

// Compose the pipeline
const getUserHandler = compose(
fetchUserFromDb,
flatMapAsync(enrichUserData),
lift(formatUserResponse)
);

Data Transformation Pipeline

const processOrder = compose(
validateOrderData,
lift(calculateTotals),
flatMapAsync(checkInventory),
flatMapAsync(createOrder),
lift(formatOrderResponse)
);

const createOrderRoute = withRoute({
method: 'POST',
path: '/orders',
handler: processOrder,
});

Error-Safe Validation Chain

import { Result, ok, err } from 'neverthrow';
import { primitives } from '@nodeblocks/backend-sdk';

const { flatMap, flatMapAsync, lift } = primitives;

// Step 1: Validate input
const validateUserInput = (data: any): Result<ValidatedUser, ValidationError> => {
if (!data.email || !data.name) {
return err(new ValidationError('Missing required fields'));
}
return ok(data);
};

// Step 2: Check if user exists
const checkUserExists = async (user: ValidatedUser): Promise<Result<User, DatabaseError>> => {
const existing = await db.users.findOne({ email: user.email });
if (existing) {
return err(new DatabaseError('User already exists'));
}
return ok(user);
};

// Step 3: Save user
const saveUser = async (user: ValidatedUser): Promise<Result<User, DatabaseError>> => {
try {
const saved = await db.users.create(user);
return ok(saved);
} catch (error) {
return err(new DatabaseError('Failed to save user'));
}
};

// Compose the error-safe pipeline
const createUserHandler = compose(
(payload) => validateUserInput(payload.params.requestBody),
flatMapAsync(checkUserExists),
flatMapAsync(saveUser),
lift(formatUserResponse)
);

📐️️ Best Practices

1. Keep Functions Small and Focused

// ✅ Good: Small, focused functions
const validateEmail = (email: string) => /* validation logic */;
const hashPassword = (password: string) => /* hashing logic */;
const saveUser = (user: User) => /* database logic */;

// ❌ Avoid: Large, multi-purpose functions
const createUserMegaFunction = (data: any) => {
// validation, hashing, saving, emailing all in one function
};

2. Handle Async Operations Properly

// ✅ Good: Use flatMapAsync for async operations
const enrichDataPipeline = compose(
fetchUserData,
flatMapAsync(fetchUserProfile), // Async operation
flatMapAsync(fetchUserSettings), // Another async operation
lift(formatResponse) // Sync transformation
);

// ❌ Avoid: Mixing async/sync without proper utilities
const badPipeline = compose(
fetchUserData,
fetchUserProfile, // This won't work properly in composition
formatResponse
);

3. Compose at the Right Level

// ✅ Good: Compose related operations
const userRegistrationFlow = compose(
validateRegistration,
createUser,
sendWelcomeEmail
);

// ✅ Good: Keep unrelated operations separate
const userLoginFlow = compose(
validateCredentials,
authenticateUser,
generateToken
);

4. Use Result Types for Error Handling

// ✅ Good: Explicit error handling with Result types
const safeOperation = compose(
validateInput,
flatMapAsync(databaseOperation), // Only runs if validation succeeds
lift(formatResponse)
);

// ❌ Avoid: Relying on thrown exceptions in composition
const unsafeOperation = compose(
validateInput,
databaseOperation, // Might throw, breaking composition
formatResponse
);

🔗 See Also