🔍 Validators
Validators are functions that perform additional validation beyond schema validation. They run before your route handlers and can check business logic, resource existence, authentication, and more.
🔍 What are Validators?
Validators are predicate functions that run in the request pipeline before handlers. They can:
- Validate business logic (e.g., "does this resource exist?")
- Check authentication (e.g., "is the user authorized?")
- Validate parameters (e.g., "is this a valid UUID?")
- Enforce constraints (e.g., "does the user have permission?")
┌─────────────┐
│ Request │
├─────────────┤
│ Schema │ ← JSON Schema validation
│ Validators │ ← Custom business logic
│ Handler │ ← Your route logic
└─────────────┘
Key characteristics:
- Async functions - Can perform database queries
- Throw on failure - Use
NodeblocksError
for consistent error responses - Composable - Chain multiple validators together
- Context-aware - Access database, request params, and context
📋 Available Validators
Generic Validators
Important: Legacy parameter validators
The following parameter validators currently use a legacy interface and will be deprecated and removed later:
requireParam
isUUID
isNumber
They do not accept the standard
RouteHandlerPayload
. You must extractpayload.params.requestParams
and pass it manually via a small wrapper:// Wrapper pattern for legacy parameter validators
({ params: { requestParams } }) => requireParam('userId')(requestParams)
({ params: { requestParams } }) => isUUID('userId')(requestParams)
({ params: { requestParams } }) => isNumber('limit')(requestParams)Because of this legacy interface, these validators cannot be passed directly to composition helpers like
compose()
orsome()
. Use standard validators or custom validators with the normal payload signature in those helpers.
requireParam(key: string)
Ensures a required parameter is present and not null.
import { validators } from '@nodeblocks/backend-sdk';
const { requireParam } = validators;
const getUserRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators: [({params: { requestParams }}) => requireParam('userId')(requestParams)],
handler: getUserHandler,
});
Behavior:
- ✅ Passes if parameter exists and is not null
- ❌ Throws error if parameter is missing or null
- Error:
Missing required parameter: userId
isUUID(key: string)
Validates that a parameter follows valid UUID v4 format.
import { validators } from '@nodeblocks/backend-sdk';
const { isUUID } = validators;
const getCategoryRoute = withRoute({
method: 'GET',
path: '/categories/:categoryId',
validators: [({params: { requestParams }}) => isUUID('categoryId')(requestParams)],
handler: getCategoryHandler,
});
Behavior:
- ✅ Passes if parameter is valid UUID v4 format
- ❌ Throws error if parameter exists but is invalid UUID
- Error:
Invalid UUID format for parameter: categoryId
- Format:
xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx
isNumber(key: string)
Validates that a parameter can be converted to a valid number.
import { validators } from '@nodeblocks/backend-sdk';
const { isNumber } = validators;
const getProductsRoute = withRoute({
method: 'GET',
path: '/products',
validators: [({params: { requestParams }}) => isNumber('limit')(requestParams)],
handler: getProductsHandler,
});
Behavior:
- ✅ Passes if parameter can be converted to number
- ❌ Throws error if parameter exists but cannot be parsed as number
- Error:
Parameter limit must be a number
some(...validators: Validator[])
Runs multiple validators and passes if at least one succeeds. Useful for conditional validation scenarios.
import { validators } from '@nodeblocks/backend-sdk';
const { some } = validators;
const flexibleUserRoute = withRoute({
method: 'GET',
path: '/users/:identifier',
validators: [
// Use standard validators here. Legacy parameter validators like
// requireParam/isUUID/isNumber cannot be used directly with `some()`.
some(validateHasUserId, validateHasEmail)
],
handler: getUserHandler,
});
Behavior:
- ✅ Passes if at least one validator succeeds
- ❌ Throws error if all validators fail
- Use case: Alternative validation paths (e.g., user can be identified by ID OR email)
Authentication Validators
verifyAuthentication
Higher-order authentication validator for protecting routes.
import { validators } from '@nodeblocks/backend-sdk';
const { verifyAuthentication } = validators;
const getSecretRoute = withRoute({
method: 'GET',
path: '/secret',
validators: [verifyAuthentication(authenticateFunction)],
handler: getSecretHandler,
});
Behavior:
- ✅ Passes if authentication succeeds
- ❌ Throws error if authentication fails
- Usage: Requires authentication function to be provided
Authentication Functions
Authentication functions are functions that validate tokens and return token information. They're used by validators to check if requests are properly authenticated.
getBearerTokenInfo
(Default)
The default authentication function that validates bearer tokens from the Authorization header.
import { validators, utils } from '@nodeblocks/backend-sdk';
const { validateResourceAccess } = validators;
const { getBearerTokenInfo } = utils;
// Default usage (getBearerTokenInfo is used automatically)
const protectedRoute = withRoute({
method: 'GET',
path: '/protected',
validators: [
validateResourceAccess(['admin']) // Uses getBearerTokenInfo by default
],
handler: protectedHandler,
});
// Explicit usage
const explicitRoute = withRoute({
method: 'GET',
path: '/protected',
validators: [
validateResourceAccess(['admin'], getBearerTokenInfo)
],
handler: protectedHandler,
});
How it works:
- Extracts token from
Authorization: Bearer <token>
header - Decrypts and verifies the JWT token signature
- Validates token type (user access token or app access token)
- Performs security checks for user tokens (fingerprint, IP, user agent)
- Returns token information for authorization decisions
Supported token types:
- User Access Tokens: Contains
userId
, requires security validation - App Access Tokens: Contains
appId
, bypasses security checks
getCookieTokenInfo
Alternative authentication function for cookie-based tokens.
import { validators } from '@nodeblocks/backend-sdk';
const { getCookieTokenInfo, validateResourceAccess } = validators;
const cookieRoute = withRoute({
method: 'GET',
path: '/cookie-protected',
validators: [
validateResourceAccess(['admin'], getCookieTokenInfo)
],
handler: protectedHandler,
});
Use cases:
- Web applications with cookie-based authentication
- Different security validation rules (IP checks disabled)
- Server-side session management
Custom Authentication Functions
You can create custom authentication functions for specific use cases:
import { primitives, validators } from '@nodeblocks/backend-sdk';
const { Authenticator, RouteHandlerPayload, TokenInfo, NodeblocksError } = primitives;
const { validateResourceAccess } = validators;
const customAuth: Authenticator = async (payload: RouteHandlerPayload): Promise<TokenInfo> => {
const { context } = payload;
const { request } = context;
// Custom token extraction logic
const customToken = request.headers['x-custom-token'];
if (!customToken) {
throw new NodeblocksError(401, 'Custom token required');
}
// Custom validation logic
// ... your validation code here
// Return token information
return {
accessType: 'user',
userId: 'user-123',
type: 'access'
};
};
// Usage with custom authentication
const customRoute = withRoute({
method: 'GET',
path: '/custom-protected',
validators: [
validateResourceAccess(['admin'], customAuth)
],
handler: protectedHandler,
});
Authentication function signature:
type Authenticator = (payload: RouteHandlerPayload) => Promise<TokenInfo>;
Domain-Specific Validators
Category Service
doesCategoryExist
Validates that a category exists in the database before performing operations.
import { validators } from '@nodeblocks/backend-sdk';
const { doesCategoryExist } = validators;
const updateCategoryRoute = withRoute({
method: 'PATCH',
path: '/categories/:categoryId',
validators: [doesCategoryExist],
handler: updateCategoryHandler,
});
Behavior:
- ✅ Passes if category exists in database
- ❌ Throws
NodeblocksError
(404) if category not found - Error:
Category does not exist
(404 status)
Access Control Validators
validateResourceAccess
Validates resource access based on allowed subjects and token information. Implements role-based access control for protected resources.
import { validators } from '@nodeblocks/backend-sdk';
const { validateResourceAccess } = validators;
const adminRoute = withRoute({
method: 'GET',
path: '/admin/users',
validators: [
validateResourceAccess(['admin'])
],
handler: getUsersHandler,
});
const selfAccessRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators: [
validateResourceAccess(['self', 'admin'])
],
handler: getUserHandler,
});
Configuration:
{
allowedSubjects: string[] // Array of allowed user types/subjects
}
Supported Subjects:
'admin'
- Administrator access'user'
- Regular user access'guest'
- Guest access'self'
- User can only access their own resources
Behavior:
- ✅ Passes if user has appropriate permissions
- ❌ Throws
NodeblocksError
(401) for invalid tokens - ❌ Throws
NodeblocksError
(403) for unauthorized access - Errors:
App token is not valid
(401)User token is not valid
(401)Identity not found
(401)User is not authorized to access this resource
(403)
validateChannelAccess
Validates channel access based on ownership and token information.
import { validators } from '@nodeblocks/backend-sdk';
const { validateChannelAccess } = validators;
const channelRoute = withRoute({
method: 'PATCH',
path: '/channels/:channelId',
validators: [
validateChannelAccess(['owner'])
],
handler: updateChannelHandler,
});
Supported Subjects:
'owner'
- Channel owner access
Behavior:
- ✅ Passes if user is channel owner
- ❌ Throws
NodeblocksError
(401) for invalid tokens - ❌ Throws
NodeblocksError
(403) for unauthorized access - ❌ Throws
NodeblocksError
(404) for channel not found - Errors:
App token is not valid
(401)User token is not valid
(401)Channel not found
(404)Channel has no owner
(403)User is not authorized to access this channel
(403)
validateMessageAccess
Validates message access based on ownership and token information.
import { validators } from '@nodeblocks/backend-sdk';
const { validateMessageAccess } = validators;
const messageRoute = withRoute({
method: 'DELETE',
path: '/messages/:messageId',
validators: [
validateMessageAccess(['owner'])
],
handler: deleteMessageHandler,
});
Supported Subjects:
'owner'
- Message sender access
Behavior:
- ✅ Passes if user is message sender
- ❌ Throws
NodeblocksError
(401) for invalid tokens - ❌ Throws
NodeblocksError
(403) for unauthorized access - ❌ Throws
NodeblocksError
(404) for message not found - Errors:
App token is not valid
(401)User token is not valid
(401)Message not found
(404)Message has no sender
(403)User is not authorized to access this message
(403)
validateOrderAccess
Validates order access based on ownership and token information.
import { validators } from '@nodeblocks/backend-sdk';
const { validateOrderAccess } = validators;
const orderRoute = withRoute({
method: 'GET',
path: '/orders/:orderId',
validators: [
validateOrderAccess(['owner'])
],
handler: getOrderHandler,
});
Supported Subjects:
'owner'
- Order owner access
Behavior:
- ✅ Passes if user is order owner
- ❌ Throws
NodeblocksError
(401) for invalid tokens - ❌ Throws
NodeblocksError
(403) for unauthorized access - ❌ Throws
NodeblocksError
(404) for order not found - Errors:
App token is not valid
(401)User token is not valid
(401)Order not found
(404)Order has no user
(403)User is not authorized to access this order
(403)
validateUserProfileAccess
Validates user profile access based on user type and ownership.
import { validators } from '@nodeblocks/backend-sdk';
const { validateUserProfileAccess } = validators;
const profileRoute = withRoute({
method: 'GET',
path: '/users/:userId/profile',
validators: [
validateUserProfileAccess(['self', 'admin'])
],
handler: getUserProfileHandler,
});
Supported Subjects:
'admin'
- Administrator access'user'
- Regular user access'guest'
- Guest access'self'
- User can access their own profile
Behavior:
- ✅ Passes if user has appropriate permissions
- ❌ Throws
NodeblocksError
(401) for invalid tokens - ❌ Throws
NodeblocksError
(403) for unauthorized access - ❌ Throws
NodeblocksError
(404) for user profile not found - Errors:
App token is not valid
(401)User token is not valid
(401)Identity not found
(401)User profile not found
(404)Identity is not authorized to access this resource
(403)
validateOrganizationAccess
Validates organization access based on membership and role.
import { validators } from '@nodeblocks/backend-sdk';
const { validateOrganizationAccess } = validators;
const orgRoute = withRoute({
method: 'GET',
path: '/organizations/:organizationId',
validators: [
validateOrganizationAccess(['admin', 'member'])
],
handler: getOrganizationHandler,
});
Supported Subjects:
'admin'
- Organization administrator'member'
- Organization member'owner'
- Organization owner
Behavior:
- ✅ Passes if user is organization member with appropriate role
- ❌ Throws
NodeblocksError
(401) for invalid tokens - ❌ Throws
NodeblocksError
(403) for unauthorized access - ❌ Throws
NodeblocksError
(404) for organization not found - Errors:
App token is not valid
(401)User token is not valid
(401)Organization not found
(404)Organization has no users
(403)User does not belong to this organization
(403)User is not authorized to access this organization
(403)
🛠️ Creating Custom Validators
Validator Function Signature
type Validator = (payload: RouteHandlerPayload) => Promise<void>;
Basic Validator Example
import { primitives } from '@nodeblocks/backend-sdk';
const { NodeblocksError } = primitives;
const validateUserExists = async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const userId = params.requestParams?.userId;
const user = await context.db.users.findOne({ id: userId });
if (!user) {
throw new NodeblocksError(404, 'User not found', 'USER_NOT_FOUND');
}
};
Advanced Validator Example
import { primitives } from '@nodeblocks/backend-sdk';
const { NodeblocksError, RouteHandlerPayload } = primitives;
const validateUserPermission = async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const { userId, organizationId } = params.requestParams;
// Check if user exists
const user = await context.db.users.findOne({ id: userId });
if (!user) {
throw new NodeblocksError(404, 'User not found', 'USER_NOT_FOUND');
}
// Check if user belongs to organization
const membership = await context.db.memberships.findOne({
userId,
organizationId,
status: 'active'
});
if (!membership) {
throw new NodeblocksError(403, 'Access denied', 'ACCESS_DENIED');
}
};
Factory Function Pattern
Create reusable validator factories:
const requireResourceExists = (collectionName: string, paramName: string) => {
return async (payload: RouteHandlerPayload) => {
const { context, params } = payload;
const resourceId = params.requestParams?.[paramName];
const resource = await context.db[collectionName].findOne({ id: resourceId });
if (!resource) {
throw new NodeblocksError(
404,
`${collectionName} not found`,
'RESOURCE_NOT_FOUND'
);
}
};
};
// Usage
const validateProductExists = requireResourceExists('products', 'productId');
const validateOrderExists = requireResourceExists('orders', 'orderId');
🔗 Using Validators in Routes
Single Validator
export const getCategoryRoute = withRoute({
method: 'GET',
path: '/categories/:categoryId',
validators: [doesCategoryExist],
handler: getCategoryHandler,
});
Multiple Validators
export const updateUserRoute = withRoute({
method: 'PATCH',
path: '/users/:userId',
validators: [
({params: { requestParams }}) => requireParam('userId')(requestParams),
({params: { requestParams }}) => isUUID('userId')(requestParams),
validateUserExists,
verifyAuthentication(authenticate)
],
handler: updateUserHandler,
});
Authentication + Authorization
export const protectedUserRoute = withRoute({
method: 'PATCH',
path: '/users/:userId',
validators: [
({params: { requestParams }}) => requireParam('userId')(requestParams),
({params: { requestParams }}) => isUUID('userId')(requestParams),
verifyAuthentication(authenticate),
validateResourceAccess(['admin', 'self'])
],
handler: updateUserHandler,
});
Conditional Validators
const validators = [
({params: { requestParams }}) => requireParam('userId')(requestParams),
({params: { requestParams }}) => isUUID('userId')(requestParams),
// Only check authentication for non-public routes
...(isPublicRoute ? [] : [verifyAuthentication(authenticate)])
];
export const getUserRoute = withRoute({
method: 'GET',
path: '/users/:userId',
validators,
handler: getUserHandler,
});
Alternative Validation Paths
export const flexibleUserRoute = withRoute({
method: 'GET',
path: '/users/:identifier',
validators: [
// Use standard validators here. Legacy parameter validators like
// requireParam/isUUID/isNumber cannot be used directly with `some()`.
some(validateHasUserId, validateHasEmail),
validateUserExists
],
handler: getUserHandler,
});
🚨 Error Handling
Using NodeblocksError
Always use NodeblocksError
for consistent error responses:
import { primitives } from '@nodeblocks/backend-sdk';
const { NodeblocksError, RouteHandlerPayload } = primitives;
const validateResource = async (payload: RouteHandlerPayload) => {
// Your validation logic
if (!isValid) {
throw new NodeblocksError(
400, // HTTP status code
'Validation failed', // Human-readable message
'VALIDATION_ERROR' // Error code for client handling
);
}
};
Error Response Format
When validators throw errors, clients receive:
{
"error": {
"message": "Category does not exist"
}
}
📋 Validator Reference
Validator | Purpose | Parameters | Returns | Throws |
---|---|---|---|---|
requireParam | Check parameter presence | key: string | Function that validates request params | Error if null/missing |
isUUID | Validate UUID format | key: string | Function that validates request params | Error if invalid UUID |
isNumber | Validate numeric format | key: string | Function that validates request params | Error if not numeric |
some | Alternative validation paths | ...validators: Validator[] | Validator | Error if all fail |
doesCategoryExist | Check category exists | none | Validator | NodeblocksError (404) |
verifyAuthentication | Validate authentication | authenticate function | Validator | Auth error |
validateResourceAccess | Check resource permissions | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403) |
validateChannelAccess | Check channel ownership | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateMessageAccess | Check message ownership | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateOrderAccess | Check order ownership | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateUserProfileAccess | Check user profile access | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
validateOrganizationAccess | Check organization access | allowedSubjects: string[], authenticate function | Validator | NodeblocksError (401/403/404) |
📐 Best Practices
1. Fail Fast
- Validators run before handlers, so fail early for invalid requests
- Don't perform expensive operations in validators unless necessary
2. Reusable Patterns
- Create factory functions for common validation patterns
- Use domain-specific validators for business logic
3. Clear Error Messages
- Provide descriptive error messages
- Use consistent error codes for client-side handling
4. Database Efficiency
- Use database indexes for validator queries
- Consider caching for frequently accessed data
5. Composition
- Chain validators logically (required → format → business logic)
- Keep validators focused on single responsibilities
6. Access Control
- Use appropriate access control validators for protected resources
- Combine authentication and authorization validators
- Consider using
some()
for flexible validation scenarios
➡️ Next
- Learn about Route Composition for building complex routes
- Explore Error Handling for consistent error responses
- Check out Service Patterns for organizing your API