Skip to main content
Version: 0.4.2 (Previous)

๐Ÿšฆ 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โ€‹

src/routes/user.ts
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:

VerbPurpose
GETRead/retrieve resources
POSTCreate new resources
PATCHPartially update resources
DELETERemove 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:

  1. Handlers - Perform business logic (database operations, validations, transformations)
  2. 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โ€‹

  1. Path semantics โ€“ nouns for resources (/users), verbs for actions (/users/:id/lock).
  2. Complete handler chains โ€“ always end with a terminator handler that formats the final response.
  3. Keep handlers small โ€“ prefer multiple composables to a single monolith.
  4. Validators first โ€“ do lightweight checks before DB calls.
  5. 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