🔧 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 objectdata
: Object containing data to merge intopayload.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
- Handler Wrappers - Cross-cutting concerns like logging and pagination
- Handler Component - Basic handler concepts
- Route Component - Route definitions
- Error Handling - Result types and error patterns
- Functional Programming - Core composition concepts