Skip to main content
Version: 0.4.2 (Previous)

🧮 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