๐ฆ 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. Routes support both HTTP and WebSocket protocols for real-time communication.
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'; // Required for HTTP routes
protocol?: 'http' | 'ws'; // Specify protocol (defaults to 'http')
path: string; // Express-style path, supports :params
validators?: Validator[]; // optional list executed **before** handler chain
handler: RouteHandler; // composable function chain
}
Protocolโ
Routes support two protocols:
Protocol | Purpose | Handler Return Type |
---|---|---|
http | Traditional REST API endpoints | Data object or Result |
ws | Real-time WebSocket connections | RxJS Subject |
Methodโ
The HTTP verb that follows REST conventions (only for HTTP routes):
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๏ธโฃ WebSocket Routesโ
WebSocket routes enable real-time communication by returning RxJS Subjects instead of static data:
import { Subject } from 'rxjs';
import { withRoute } from '../primitives';
export const realtimeDataRoute = withRoute({
protocol: 'ws',
path: '/api/realtime',
handler: (payload) => {
const subject = new Subject();
// Setup real-time data source (e.g., database change stream)
const changeStream = payload.context.db.data.watch();
changeStream.on('change', (changeDoc) => {
subject.next({
type: 'data_change',
data: changeDoc.fullDocument,
timestamp: Date.now()
});
});
// Cleanup when connection closes
subject.subscribe({
complete: () => changeStream.close(),
error: () => changeStream.close()
});
return subject;
},
validators: [
// WebSocket routes can also use validators
(payload) => {
// Validate WebSocket connection headers, etc.
return Promise.resolve();
}
]
});
WebSocket vs HTTP Routesโ
Aspect | HTTP Routes | WebSocket Routes |
---|---|---|
Protocol | 'http' (default) | 'ws' |
Method | Required (GET , POST , etc.) | Not applicable |
Handler Return | Data object or Result | RxJS Subject |
Communication | Request-response | Bidirectional streaming |
Use Cases | REST APIs, CRUD operations | Real-time updates, live data |
4๏ธโฃ 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, or explore WebSocket Services ยป for real-time functionality