Skip to main content
Version: ๐Ÿšง Canary

๐Ÿชช 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) or some() (OR) for flexible access rules
  • Throw NodeblocksError for 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)
LayerPurposeWhere defined
Route validatorsBusiness logic โ€” auth, permissions, existencevalidators on withRoute
Schema validationRequest shape โ€” path, query, body fieldswithSchema 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
checkIdentityTypeconfiguration.identity.typeIds, db.identities
hasOrgRole, hasOrgRoleSameOrAbove, hasOrgRoleAssignmentPermissionconfiguration.organization.roles, db.organizations
hasOrgOwnerRemainingAfterMembersUpsert, hasOrgOwnerRemainingAfterMemberRemovalconfiguration.organization.roles, db.organizations

๐Ÿ”€ Compositionโ€‹

ExportBehavior
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โ€‹

ExportSignature
all(...validators: Validator[]) => Validator
some(...validators: Validator[]) => Validator

Authentication and identityโ€‹

ExportSignatureNotes
isAuthenticated() => ValidatorCalls context.authenticate, defaulting to getBearerTokenInfo
checkIdentityType(allowedTypes) => ValidatorAlso authenticates; requires db.identities and configuration.identity.typeIds

Organizationโ€‹

ExportSignatureNotes
hasOrgRole(allowedRoles, organizationIdPath) => ValidatorMaps role keys via configuration.organization.roles
hasOrgRoleSameOrAbove(orgIdPath, identityIdPath, rolesByRankAsc?) => ValidatorDefault rank order: ['member', 'admin', 'owner']
hasOrgRoleAssignmentPermission(orgIdPath, membersPath, rolesByRankAsc?) => ValidatorValidates role assignment on member upsert
hasOrgOwnerRemainingAfterMembersUpsert(orgIdPath, membersPath) => ValidatorOrg invariant only โ€” no authentication
hasOrgOwnerRemainingAfterMemberRemoval(orgIdPath, identityIdPath) => ValidatorOrg 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:

ExportCollectionOwner field
ownsProfileprofilesidentityId
ownsOrderordersidentityId
ownsChannelchatChannelsownerId
ownsMessagechatMessagessenderId
ownsSubscriptionsubscriptionssubscribedId
ownsNotificationnotificationsreceiverId

Most ownership validators authenticate internally. SDK routes often still include isAuthenticated() for clarity.

Chatโ€‹

ExportSignatureNotes
hasSubscription(channelIdPath, subscribedIdPath?) => ValidatorDefaults subscribedId to token identityId when path omitted
hasOrganizationAccessToMessageTemplate(allowedRoles, messageTemplateIdPath) => ValidatorCompares raw member.role strings from DB (unlike hasOrgRole)
channelExists(channelIdPath) => ValidatorExistence check only โ€” no authentication

Otherโ€‹

ExportSignatureNotes
isSelf(identityIdPath) => ValidatorEnsures target identity matches authenticated user
doesCategoryExistdirect ValidatorReads params.requestParams.categoryId โ€” not a factory

Utility functionsโ€‹

These are exported alongside validators but are not route validators themselves:

ExportModulePurpose
getResourceByIdownsResourceFetch a document by id from a collection
getSubscriptionByChannelAndSubscriberhasSubscriptionFetch 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โ€‹

  1. Route validators before handler โ€” schema validates shape inside the handler wrapper; validators gate business logic first.
  2. Prefer SDK validators โ€” use hasOrgRole, ownsProfile, etc. instead of ad-hoc DB checks.
  3. Call factories โ€” isAuthenticated(), checkIdentityType([...]), hasOrgRole([...], path); use doesCategoryExist directly without ().
  4. Use some() for alternatives โ€” admin OR org owner OR resource owner access patterns.
  5. Configure type and role IDs โ€” ensure configuration.identity.typeIds and configuration.organization.roles are set when using type/role validators.
  6. Compose logically โ€” authentication โ†’ authorization โ†’ resource existence/invariants.
  7. Index your queries โ€” validators often query by id; ensure MongoDB indexes on those fields.

โžก๏ธ Nextโ€‹