๐ฆ Routes
A route bundles an HTTP method, path, validators and a business logic together. Nodeblocks provides the withRoute
helper which stores metadata and plugs into Express automatically when the service router is generated.
1๏ธโฃ Anatomyโ
import {compose, flatMapAsync, withRoute, lift} from '../primitives';
import {createUser, getUserById, normalizeUserTerminator} from '../handlers';
export const createUserRoute = withRoute({
method: 'POST',
path: '/users',
validators: [verifyAuthentication(getBearerTokenInfo)], // optional array of validator functions
handler: compose(
createUser, // Handler: write to DB
flatMapAsync(getUserById), // Handler: read back the created entity
lift(normalizeUserTerminator) // Terminator handler: format final HTTP response
),
});
Below is the minimal shape returned by withRoute
.
interface Route {
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
path: string; // Express-style path, supports :params
validators?: Validator[]; // optional list executed **before** handler chain
handler: RouteHandler; // composable function chain
}
Methodโ
The HTTP verb that follows REST conventions:
Verb | Purpose |
---|---|
GET | Read/retrieve resources |
POST | Create new resources |
PATCH | Partially update resources |
DELETE | Remove resources |
Pathโ
An Express-style template. Use plural nouns for collections (/users
) and sub-paths for actions (/users/:id/lock
). Dynamic parts become requestParams
.
Validatorsโ
Synchronous or async hooks that run before any database work. They receive the same payload
object.
The idiomatic way to reject a request is to throw new NodeblocksError(status, message)
:
import {NodeblocksError} from '../primitives/error';
export const validateEmail: Validator = ({ params }) => {
if (!params.requestBody.email?.includes('@')) {
throw new NodeblocksError(400, 'Invalid email');
}
};
Handlerโ
Routes delegate their business logic to handler chainsโcomposed functions that process requests and format responses. Every handler chain follows this pattern:
- Handlers - Perform business logic (database operations, validations, transformations)
- Terminator Handler - Final handler that formats the HTTP response
handler: compose(
handler1, // Returns Result<RouteHandlerPayload, Error>
flatMapAsync(handler2), // Returns Result<RouteHandlerPayload, Error>
lift(terminatorHandler) // Handles Result, returns formatted response
)
Handlers have their own dedicated explanation covering signatures, mergeData
, Result patterns, and terminator requirements. โ Handler explanation
2๏ธโฃ Good Practicesโ
- Path semantics โ nouns for resources (
/users
), verbs for actions (/users/:id/lock
). - Complete handler chains โ always end with a terminator handler that formats the final response.
- Keep handlers small โ prefer multiple composables to a single monolith.
- Validators first โ do lightweight checks before DB calls.
- Stateless โ routes should not mutate global state; everything arrives via
payload
.
Handler Chain Patternโ
handler: compose(
handler1, // Business logic: returns Result
flatMapAsync(handler2), // More business logic: returns Result
lift(terminatorHandler) // Response formatting: handles Result, returns data
)
3๏ธโฃ Adding Validatorsโ
import {withRoute, compose} from '../primitives';
import {NodeblocksError} from '../primitives/error';
import {ok, err} from 'neverthrow';
import {mergeData} from '../handlers';
// Custom validator that always blocks access
const youShallNotPass = () => {
throw new NodeblocksError(400, 'YOU SHALL NOT PASS!');
};
// Handler for getting secret data
const getSecretData = async (payload) => {
return ok(mergeData(payload, { hiddenSecret: '๐งฑ๐งฑ๐๐งฑ๐งฑ' }));
};
// Terminator handler for formatting response
const secretTerminator = (result) => {
if (result.isErr()) {
throw result.error;
}
const payload = result.value;
const { context } = payload;
return { secret: context.data.secret };
};
export const getSecretRoute = withRoute({
method: 'GET',
path: '/secret',
validators: [youShallNotPass],
handler: compose(
getSecretData, // Handler: get the data
lift(secretTerminator) // Terminator: format final response
),
});
Validators throw NodeblocksError
on failure which bubbles up to the error middleware.
โก๏ธ Nextโ
Learn about Feature ยป to see how to compose routes and schemas into reusable units