🧮 Functional Programming Concepts
Nodeblocks backend SDK is built on functional programming principles. Understanding these mathematical concepts will help you write more elegant, composable, and maintainable code.
🔍 What is Functional Programming?
Functional programming is a programming paradigm that treats computation as the evaluation of mathematical functions. In Nodeblocks, we use functional programming to:
- Compose complex operations from simple functions
- Avoid mutable state and side effects
- Create predictable, testable code
- Build modular, reusable components
📐 Core Mathematical Concepts
Function Composition
Function composition is the mathematical operation of combining two functions to produce a third function.
Mathematical Definition:
(f ∘ g)(x) = f(g(x))
In Nodeblocks:
import { compose } from '@nodeblocks/backend-sdk';
// Instead of:
const result = f(g(x));
// We use composition:
const composedFunction = compose(f, g);
const result = composedFunction(x);
Real Example:
// Create a user feature: validate → save → format response
const createUserFeature = compose(
validateUserSchema, // f: validate input
createUserHandler, // g: save to database
formatUserResponse // h: format output
);
// This is equivalent to: formatUserResponse(createUserHandler(validateUserSchema(input)))
Benefits:
- Readable: Functions flow from left to right
- Composable: Easy to add/remove steps
- Testable: Each function can be tested independently
Currying
Currying is the technique of converting a function that takes multiple arguments into a sequence of functions that each take a single argument.
Mathematical Definition:
f(x, y, z) → f(x)(y)(z)
In Nodeblocks:
import { curry } from 'ramda';
// Regular function
const add = (a, b) => a + b;
// Curried function
const curriedAdd = curry(add);
const addFive = curriedAdd(5);
const result = addFive(3); // 8
Real Example - Authentication Validator:
// Without currying (needs all arguments at once)
const verifyAuth = (authFunction, payload) => {
return authFunction(payload);
};
// With currying (can be partially applied)
const verifyAuthentication = curry((authFunction, payload) => {
return authFunction(payload);
});
// Usage: partially apply the auth function
const validateWithJWT = verifyAuthentication(jwtAuthFunction);
// Then use in routes
const protectedRoute = withRoute({
validators: [validateWithJWT], // Only needs payload now
handler: secretHandler
});
Benefits:
- Partial Application: Create specialized functions
- Reusability: Same function, different configurations
- Composability: Easy to combine with other functions
Partial Application
Partial application is the process of fixing a number of arguments to a function, producing another function of smaller arity.
Mathematical Definition:
f(x, y, z) → f(x, y, _) → g(z)
In Nodeblocks:
import _ from 'lodash';
// Original function
const multiply = (a, b) => a * b;
// Partially apply first argument
const multiplyByTwo = _.partial(multiply, 2);
const result = multiplyByTwo(5); // 10
Real Example - Service Configuration:
// Service needs database and configuration
const authService = (db, config) => {
return defService(_.partial(compose(feature1, feature2), { dataStores: db, ...config }));
};
// Partially apply configuration
const authServiceWithConfig = _.partial(authService, _, {
maxFailedLoginAttempts: 5,
accessTokenExpireTime: '2h'
});
// Now only needs database
app.use('/auth', authServiceWithConfig(database));
Benefits:
- Configuration: Set up functions with default parameters
- Flexibility: Same function, different configurations
- Cleaner Code: Less repetition
Higher-Order Functions
Higher-order functions are functions that either take functions as arguments or return functions as results.
Mathematical Definition:
H(f) = g, where f and g are functions
In Nodeblocks:
// Function that returns a function
const createValidator = (errorMessage) => {
return (value) => {
if (!value) {
throw new NodeblocksError(400, errorMessage);
}
};
};
// Usage
const requireUserId = createValidator('User ID is required');
const requireEmail = createValidator('Email is required');
Real Example - Route Factory:
// Higher-order function that creates routes
const createCRUDRoute = (handler, validators = []) => {
return withRoute({
handler,
validators,
method: 'POST',
path: '/items'
});
};
// Usage
const createUserRoute = createCRUDRoute(createUserHandler, [validateUser]);
const createProductRoute = createCRUDRoute(createProductHandler, [validateProduct]);
🔧 Nodeblocks Functional Patterns
Result Types (Monads)
Nodeblocks uses Result
types to handle success and failure cases explicitly.
import { Result, ok, err } from 'neverthrow';
// Functions return Results instead of throwing
const validateUser = (data): Result<User, ValidationError> => {
if (!data.email) {
return err(new ValidationError('Email required'));
}
return ok(new User(data));
};
const saveUser = async (user: User): Promise<Result<User, DatabaseError>> => {
try {
const saved = await db.users.insert(user);
return ok(saved);
} catch (error) {
return err(new DatabaseError(error.message));
}
};
// Compose with flatMap
const createUser = compose(
validateUser,
flatMapAsync(saveUser)
);
Function Lifting
Lifting transforms regular functions to work with Results.
import { lift } from '@nodeblocks/backend-sdk';
// Regular function
const formatResponse = (user) => ({
success: true,
data: user
});
// Lifted function works with Results
const formatResponseLifted = lift(formatResponse);
// Usage in composition
const createUserFeature = compose(
validateUser,
flatMapAsync(saveUser),
lift(formatResponse) // Lifts regular function to work with Results
);
Pipeline Composition
Nodeblocks uses pipelines to chain operations together.
// Pipeline: Input → Validation → Business Logic → Response
const userFeature = compose(
// 1. Validate input
validateUserSchema,
// 2. Business logic
flatMapAsync(createUser),
flatMapAsync(sendWelcomeEmail),
// 3. Format response
lift(formatUserResponse)
);
🧮 Mathematical Foundations
Category Theory
Nodeblocks patterns are inspired by category theory concepts:
Functors:
// Result is a functor - it can be mapped over
const userResult = ok({ name: 'John', email: 'john@example.com' });
const formattedResult = userResult.map(user => ({
...user,
displayName: user.name.toUpperCase()
}));
Monads:
// Result is a monad - it can be chained
const result = ok(5)
.andThen(x => ok(x * 2))
.andThen(x => ok(x + 1));
// Result: ok(11)
Algebraic Data Types
Nodeblocks uses algebraic data types for modeling:
// Sum type (union)
type ValidationResult =
| { type: 'success'; data: User }
| { type: 'error'; message: string };
// Product type (tuple/object)
type RouteConfig = {
method: string;
path: string;
handler: Function;
validators: Function[];
};
📐 Best Practices
1. Pure Functions
- Functions should have no side effects
- Same input always produces same output
- Easy to test and reason about
// ✅ Pure function
const add = (a, b) => a + b;
// ❌ Impure function (side effect)
const addAndLog = (a, b) => {
console.log('Adding:', a, b); // Side effect
return a + b;
};
2. Immutability
- Don't modify existing data
- Create new data structures instead
// ✅ Immutable
const updateUser = (user, updates) => ({
...user,
...updates
});
// ❌ Mutable
const updateUser = (user, updates) => {
Object.assign(user, updates); // Modifies original
return user;
};
3. Function Composition
- Build complex operations from simple functions
- Keep functions focused and single-purpose
// ✅ Composed
const processUser = compose(
validateUser,
flatMapAsync(saveUser),
lift(formatResponse)
);
// ❌ Monolithic
const processUser = async (data) => {
// 100 lines of mixed concerns
};
4. Error Handling with Results
- Use Results instead of exceptions for expected errors
- Make error handling explicit
// ✅ Explicit error handling
const result = await createUser(userData);
if (result.isErr()) {
return handleError(result.error);
}
return result.value;
// ❌ Implicit error handling
try {
const user = await createUser(userData);
return user;
} catch (error) {
// What type of error? Expected or unexpected?
}
➡️ Next
- Learn about Route Composition to see these concepts in action
- Explore Service Patterns for functional service design
- Check out Validators for functional validation patterns