🪪 Validator
Validators are functions attached to routes via withRoute({ validators }). HTTP validators are asynchronous; WebSocket validators may be synchronous or asynchronous. They perform business-logic checks beyond schema validation — authentication, authorization, resource existence, and ownership.
There is no validator registration API. Import from the validators namespace and attach them to route composers in src/routes/.
import { validators } from '@nodeblocks/backend-sdk';
type Validator = (payload: RouteHandlerPayload) => Promise<void>;
type WsValidator = (payload: WsRouteHandlerPayload) => void | Promise<void>;
Factory validators must be called to produce a Validator (e.g. isAuthenticated(), not isAuthenticated). Direct validators like doesCategoryExist are used as-is — do not call them with ().
🔍 What are Validators?
Validators run in the request pipeline before handler business logic. They receive the same payload object as route handlers (RouteHandlerPayload for HTTP, WsRouteHandlerPayload for WebSocket).
Why use validators:
- Enforce auth and access control before DB-heavy handler chains
- Reuse SDK predicates across routes (
hasOrgRole,ownsProfile, etc.) - Compose with
all()(AND) orsome()(OR) for flexible access rules - Throw
NodeblocksErrorfor consistent HTTP error responses (WebSocket failures close the connection)
📐 Schema vs Validators
Two validation layers run at different points. See Schema » for shape validation via withSchema.
HTTP request
→ route validators (defService)
→ handler entry
→ schema validation (withNextRoute wrapper)
→ handler chain (blocks / handlers)
| Layer | Purpose | Where defined |
|---|---|---|
| Route validators | Business logic — auth, permissions, existence | validators on withRoute |
| Schema validation | Request shape — path, query, body fields | withSchema in features (compose(schema, route)) |
Execution order: defService runs route validators first, then invokes the handler. Schema validation runs inside the schema-wrapped handler via withNextRoute. Correct order: Validators → Schema → Handler chain.
⚙️ Context and Paths
Validators receive RouteHandlerPayload:
{
context: {
db, // mapped from service dataStores
configuration, // service config (identity type IDs, org roles)
authenticate, // defaults to getBearerTokenInfo; cookie services inject getCookieTokenInfo
// ...optional drivers (mailService, fileStorageDriver, etc.)
},
params: {
requestParams, // path parameters
requestQuery, // query string
requestBody, // JSON body (POST/PUT/PATCH/DELETE)
},
}
Path arguments use Ramda path tuples rooted at the payload. SDK routes consistently use:
['params', 'requestParams', 'profileId']
['params', 'requestBody', 'identityId']
['params', 'requestQuery', 'channelId']
When using cookie authentication, services inject getCookieTokenInfo as context.authenticate. Register cookie-parser middleware before mounting the service router.
Configuration dependencies
Some validators require service configuration:
| Validator(s) | Required config |
|---|---|
checkIdentityType | configuration.identity.typeIds, db.identities |
hasOrgRole, hasOrgRoleSameOrAbove, hasOrgRoleAssignmentPermission | configuration.organization.roles, db.organizations |
hasOrgOwnerRemainingAfterMembersUpsert, hasOrgOwnerRemainingAfterMemberRemoval | configuration.organization.roles, db.organizations |
🔀 Composition
| Export | Behavior |
|---|---|
all(...validators) | Sequential AND — runs validators one by one, fail-fast on first throw |
some(...validators) | Concurrent OR — runs all validators via Promise.allSettled; passes if any succeeds; if all fail, throws the first collected error in input order |
Many SDK routes use some(checkIdentityType(['admin']), hasOrgRole(...)) for admin-or-org-member access:
import { validators } from '@nodeblocks/backend-sdk';
const { isAuthenticated, some, checkIdentityType, hasOrgRole } = validators;
validators: [
isAuthenticated(),
some(
checkIdentityType(['admin']),
hasOrgRole(['owner', 'admin'], ['params', 'requestParams', 'organizationId'])
),
],
📦 SDK Validator Catalog
All validators are exported from the validators namespace, matching src/validators/index.ts.
Composition
| Export | Signature |
|---|---|
all | (...validators: Validator[]) => Validator |
some | (...validators: Validator[]) => Validator |
Authentication and identity
| Export | Signature | Notes |
|---|---|---|
isAuthenticated | () => Validator | Calls context.authenticate, defaulting to getBearerTokenInfo |
checkIdentityType | (allowedTypes) => Validator | Also authenticates; requires db.identities and configuration.identity.typeIds |
Organization
| Export | Signature | Notes |
|---|---|---|
hasOrgRole | (allowedRoles, organizationIdPath) => Validator | Maps role keys via configuration.organization.roles |
hasOrgRoleSameOrAbove | (orgIdPath, identityIdPath, rolesByRankAsc?) => Validator | Default rank order: ['member', 'admin', 'owner'] |
hasOrgRoleAssignmentPermission | (orgIdPath, membersPath, rolesByRankAsc?) => Validator | Validates role assignment on member upsert |
hasOrgOwnerRemainingAfterMembersUpsert | (orgIdPath, membersPath) => Validator | Org invariant only — no authentication |
hasOrgOwnerRemainingAfterMemberRemoval | (orgIdPath, identityIdPath) => Validator | Org invariant only — no authentication |
Resource ownership
Generic ownership check:
ownsResource(resource, ownerIdPathInResource, resourceIdPathInPayload)
Allowed resource collections: chatChannels, chatMessages, orders, profiles, subscriptions, notifications.
Domain wrappers are partial(ownsResource, [collection, [ownerField]]) — pass only the resource ID path:
| Export | Collection | Owner field |
|---|---|---|
ownsProfile | profiles | identityId |
ownsOrder | orders | identityId |
ownsChannel | chatChannels | ownerId |
ownsMessage | chatMessages | senderId |
ownsSubscription | subscriptions | subscribedId |
ownsNotification | notifications | receiverId |
Most ownership validators authenticate internally. SDK routes often still include isAuthenticated() for clarity.
Chat
| Export | Signature | Notes |
|---|---|---|
hasSubscription | (channelIdPath, subscribedIdPath?) => Validator | Defaults subscribedId to token identityId when path omitted |
hasOrganizationAccessToMessageTemplate | (allowedRoles, messageTemplateIdPath) => Validator | Compares raw member.role strings from DB (unlike hasOrgRole) |
channelExists | (channelIdPath) => Validator | Existence check only — no authentication |
Other
| Export | Signature | Notes |
|---|---|---|
isSelf | (identityIdPath) => Validator | Ensures target identity matches authenticated user |
doesCategoryExist | direct Validator | Reads params.requestParams.categoryId — not a factory |
Utility functions
These are exported alongside validators but are not route validators themselves:
| Export | Module | Purpose |
|---|---|---|
getResourceById | ownsResource | Fetch a document by id from a collection |
getSubscriptionByChannelAndSubscriber | hasSubscription | Fetch subscription by channelId + subscribedId |
🔗 Using Validators in Routes
Attach validators on withRoute in src/routes/. They are composed into features via compose(withSchema, withRoute).
Admin or resource owner
From src/routes/profile.ts:
import { handlers, primitives, validators } from '@nodeblocks/backend-sdk';
const { getProfileById } = handlers;
const { withRoute } = primitives;
const { isAuthenticated, some, checkIdentityType, ownsProfile } = validators;
export const getProfileRoute = withRoute({
method: 'GET',
path: '/profiles/:profileId',
validators: [
isAuthenticated(),
some(
checkIdentityType(['admin']),
ownsProfile(['params', 'requestParams', 'profileId'])
),
],
// The handler chain is omitted; validators run before it.
handler: getProfileById,
});
Organization member upsert with invariants
From src/routes/organization.ts:
import { validators } from '@nodeblocks/backend-sdk';
const {
isAuthenticated,
all,
some,
checkIdentityType,
hasOrgRole,
hasOrgRoleAssignmentPermission,
hasOrgOwnerRemainingAfterMembersUpsert,
} = validators;
validators: [
isAuthenticated(),
some(
checkIdentityType(['admin']),
all(
hasOrgRole(['owner', 'admin'], ['params', 'requestParams', 'organizationId']),
hasOrgRoleAssignmentPermission(
['params', 'requestParams', 'organizationId'],
['params', 'requestBody']
)
)
),
hasOrgOwnerRemainingAfterMembersUpsert(
['params', 'requestParams', 'organizationId'],
['params', 'requestBody']
),
],
Category existence
import { validators } from '@nodeblocks/backend-sdk';
const { isAuthenticated, doesCategoryExist } = validators;
validators: [isAuthenticated(), doesCategoryExist],
🛠️ Creating Custom Validators
import { primitives } from '@nodeblocks/backend-sdk';
const validateIdentityExists: primitives.Validator = async ({ context, params }) => {
const identityId = params.requestParams?.identityId;
const identity = await context.db.identities.findOne({ id: String(identityId) });
if (!identity) {
throw new primitives.NodeblocksError(
404,
'Identity not found',
'validateIdentityExists'
);
}
};
Factory pattern
const requireResourceExists = (collectionName: string, paramName: string) => {
return async (payload: primitives.RouteHandlerPayload) => {
const { context, params } = payload;
const resourceId = params.requestParams?.[paramName];
const resource = await context.db[collectionName].findOne({
id: String(resourceId),
});
if (!resource) {
throw new primitives.NodeblocksError(
404,
`${collectionName} not found`,
'requireResourceExists'
);
}
};
};
Reject requests by throwing NodeblocksError(status, message, source). Validators must resolve to void on success.
🌐 WebSocket Validators
WebSocket routes use WsValidator on withRoute({ protocol: 'ws', validators }). defService runs validators when a client connects, with params derived from the connection URL query string:
{ context, params: { requestQuery: { /* from URL */ } } }
Attach the same validator factories where applicable, or write custom WsValidator functions for WS-specific checks.
See Route » for WebSocket route configuration and the WebSocket Service Guide » for chat streaming setup.
🚨 Error Handling
Validators throw NodeblocksError. defService normalizes thrown errors and passes them to Express; mount nodeBlocksErrorMiddleware() at the app level to send the error response:
import { middlewares } from '@nodeblocks/backend-sdk';
const { nodeBlocksErrorMiddleware } = middlewares;
app.use(nodeBlocksErrorMiddleware());
Clients receive:
{
"error": {
"message": "Category does not exist"
}
}
stack is included only when NODE_ENV === 'development'. Optional data is included when set on the error.
See Error Handling » for the full error model.
📐 Good Practices
- Route validators before handler — schema validates shape inside the handler wrapper; validators gate business logic first.
- Prefer SDK validators — use
hasOrgRole,ownsProfile, etc. instead of ad-hoc DB checks. - Call factories —
isAuthenticated(),checkIdentityType([...]),hasOrgRole([...], path); usedoesCategoryExistdirectly without(). - Use
some()for alternatives — admin OR org owner OR resource owner access patterns. - Configure type and role IDs — ensure
configuration.identity.typeIdsandconfiguration.organization.rolesare set when using type/role validators. - Compose logically — authentication → authorization → resource existence/invariants.
- Index your queries — validators often query by
id; ensure MongoDB indexes on those fields.
➡️ Next
- Schema » — request shape validation and the schema vs validator distinction
- Route » — attaching validators to
withRouteand handler chains - Feature » — pairing schemas and routes in features
- Service » — mounting composed features via
defService - Error Handling » —
NodeblocksErrorand middleware