Centralize FeathersJS Validation with AJV Service Hooks
Use one AJV JSON Schema in a FeathersJS before hook to validate REST and Socket.io payloads before they reach the database adapter.
02 Mar 2026, 20:11 UTC

One Schema for REST and Socket.io
You build a FeathersJS service for projects. Your REST endpoint rejects a missing name field, but a Socket.io client can emit the same create event with an empty payload and reach the database adapter unchanged. Or you add validation inside create, duplicate it in patch, then forget it in a custom method. The result is inconsistent data integrity across transports.
The practical fix is to attach a single AJV-based before hook to the service. Feathers runs hooks before the adapter regardless of whether the call arrived over REST, Socket.io, or another transport, so one JSON Schema becomes the source of truth for every incoming payload. AJV is a JSON Schema validator; a Feathers hook is an async function that receives a context object and can short-circuit or reject a call.
Feathers normalizes the hook context so the same function sees REST query parameters, Socket.io data, and service method names in a consistent shape. Because the hook lives in the service's before chain, it runs for every transport without duplicating logic in controller-like code.
Write the AJV Hook
This example assumes Feathers v5 (Dove). Compile the schema once outside the hook so validation does not recompile on every request.
// src/hooks/validate-project.js
const { Ajv } = require('ajv');
const { BadRequest } = require('@feathersjs/errors');
const ajv = new Ajv({ allErrors: true, strict: false });
const schema = {
type: 'object',
required: ['name', 'budget'],
properties: {
name: { type: 'string', minLength: 1, maxLength: 100 },
budget: { type: 'integer', minimum: 0 }
},
additionalProperties: false
};
const validate = ajv.compile(schema);
module.exports = async function validateProject(context) {
if (context.method !== 'create' && context.method !== 'patch') {
return;
}
const data = context.data;
if (!data) {
return;
}
const valid = validate(data);
if (!valid) {
const details = validate.errors
.map(err => `${err.instancePath || '(root)'}: ${err.message}`)
.join('; ');
throw new BadRequest(`Invalid project data: ${details}`);
}
};
The hook skips read-only methods, checks data against the schema, and throws a Feathers BadRequest error when validation fails. Using BadRequest instead of a plain Error helps Feathers return a 400 rather than a 500.
Register It on the Service
Add the hook to the create, update, and patch methods in the service hook file.
// src/services/projects/projects.hooks.js
const validateProject = require('../../hooks/validate-project');
module.exports = {
before: {
all: [],
find: [],
get: [],
create: [validateProject],
update: [validateProject],
patch: [validateProject],
remove: []
},
after: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] },
error: { all: [], find: [], get: [], create: [], update: [], patch: [], remove: [] }
};
Verify With curl and Watch for Errors
Run these commands from the project root as a normal user. No sudo is required. Replace http://localhost:3030 with your actual server URL if needed.
Valid payload:
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:3030/projects \
-H "Content-Type: application/json" \
-d '{"name":"Bridge","budget":5000}'
Expect a 2xx success; the exact code depends on your database adapter.
Invalid payload:
curl -s -o /dev/null -w "%{http_code}" -X POST http://localhost:3030/projects \
-H "Content-Type: application/json" \
-d '{"budget":-10}'
Expect 400. If the invalid request returns anything else, check that the hook file is imported in projects.hooks.js and that the server was restarted after the change.
For real-time verification, connect a Socket.io client, emit create on the projects service with both payloads, and confirm the server responds with an error only for the invalid case. Do not return raw AJV error objects to clients in production; they can expose schema internals. A joined message string is safer, though it still reveals field paths.
Trade-Offs and Rollback
Compiling the schema once keeps overhead low, but complex nested schemas or custom keywords can still increase hook execution time. Benchmark with realistic payloads under load. additionalProperties: false rejects fields clients may legitimately send, so relax it if your API accepts arbitrary metadata. Hook errors must be Feathers errors; plain Errors can surface as 500s.
Because adding a hook changes runtime behavior, rollback is straightforward: remove validateProject from the before array in projects.hooks.js and redeploy. The service returns to its previous validation behavior immediately.
Start with your most write-heavy service, add the hook, verify with the two curl calls above, then extend to other services. A single schema in a before hook keeps REST and real-time clients under the same rule without duplicating logic in every service method.
0 replies
A thoughtful contribution can make all the difference. Be the first to share one.