メインコンテンツまでスキップ
バージョン: 🚧 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; waits for all validators; if all fail, throws the first collected error in input order (non-NodeblocksError failures become a generic 500 error); an empty list passes

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​

Simplified from src/routes/profile.ts to show the validators and a Result terminator. The SDK route also normalizes the profile and avatar before responding:

import { handlers, primitives, validators } from '@nodeblocks/backend-sdk';

const { getProfileById, normalizeProfileTerminator } = handlers;
const { compose, lift, 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'])
),
],
// Validators run before the handler chain.
handler: compose(getProfileById, lift(normalizeProfileTerminator)),
});

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​

  • Schema » — request shape validation and the schema vs validator distinction
  • Route » — attaching validators to withRoute and handler chains
  • Feature » — pairing schemas and routes in features
  • Service » — mounting composed features via defService
  • Error Handling » — NodeblocksError and middleware