Case-Study · Express 4 + TypeScript + Ajv 8.12 + ajv-formats
Case-Study: Express-API mit lückenloser Ajv-Validation
Express-API mit 14 Endpoints. Ajv pre-compile im Build-Step. Request-Validation via Middleware, Response-Validation per Vitest-Test. Resultat: 0 Schema-Drift-Bugs in 6 Monaten.
Schema · Draft 2020-12
Payload-Beispiel
{
"user_id": "usr_1k2j3h4g",
"email": "max@example.com",
"name": "Max Mustermann",
"role": "admin"
} Schema-Auszug
{
"$schema": "https://json-schema.org/draft/2020-12/schema",
"type": "object",
"additionalProperties": false,
"required": ["user_id", "email", "name", "role"],
"properties": {
"user_id": { "type": "string", "pattern": "^usr_[a-z0-9]{8}$" },
"email": { "type": "string", "format": "email" },
"name": { "type": "string", "minLength": 1, "maxLength": 100 },
"role": { "enum": ["admin", "editor", "viewer"] }
}
} Erkenntnisse aus dem Projekt
- ✓ additionalProperties: false fängt 90 % der Tipp-Fehler im Client
- ✓ Pre-Compile per ajv-cli spart 30-80 ms pro Request
- ✓ allErrors: true im Dev, allErrors: false in Prod (Performance)
- ✓ Pattern-Properties + Pflicht-Felder ergeben sauberere Fehlermeldungen als regex-only
- ✓ Response-Validation als Vitest-Smoke-Test, nicht in Middleware
Setup: B2B-API mit 14 Endpoints, ~5.000 Requests/Minute. TypeScript-Backend auf Node 22, Express 4 als Framework. Schemas leben in schemas/, pro Endpoint zwei Files: request.schema.json und response.schema.json. Validator: Ajv 8.12 mit ajv-formats.
Architektur-Übersicht
| Schicht | Was validiert wird | Wann |
|---|---|---|
| Build-Step (CI) | Schemas selbst (sind sie valides JSON Schema?) | Pre-Push, Pre-Deploy |
| Ajv pre-compile | Schemas → compile()-Funktionen | App-Start |
| Request-Middleware | Request-Body gegen request.schema | Pro Request, vor Handler |
| Response (Tests) | Sample-Responses gegen response.schema | Vitest, nicht zur Laufzeit |
Schritt 1: Schemas selbst validieren
Bevor Schemas in Production laufen, müssen sie selbst gültiges JSON Schema sein. CI-Step mit Ajv-CLI:
# package.json
"scripts": {
"validate:schemas": "ajv compile --strict=true -s 'schemas/**/*.schema.json'"
}
Das fängt typische Fehler wie "required": "name" (statt Array), falsch geschriebene Keywords (requiered), oder $ref-Loops.
Schritt 2: Schemas zur App-Start-Zeit kompilieren
import Ajv from 'ajv';
import addFormats from 'ajv-formats';
const ajv = new Ajv({ allErrors: false, removeAdditional: 'failing' });
addFormats(ajv);
const validators = new Map();
for (const file of glob.sync('schemas/**/*.schema.json')) {
const schema = JSON.parse(fs.readFileSync(file, 'utf8'));
const validate = ajv.compile(schema);
validators.set(file, validate);
}
Spart pro Request 30-80 ms gegenüber on-the-fly-compile. Ajv hält die kompilierten Funktionen im Memory.
Schritt 3: Request-Validation als Middleware
function validateRequest(schemaPath: string) {
return (req, res, next) => {
const validate = validators.get(schemaPath);
if (validate(req.body)) return next();
res.status(400).json({
error: 'invalid_payload',
details: validate.errors,
});
};
}
// Verwendung:
app.post('/users',
validateRequest('schemas/users-create.request.schema.json'),
usersController.create);
Schritt 4: Response-Validation als Test (NICHT Middleware!)
Response-Validation in Middleware kostet pro Request 5-30 ms - nicht akzeptabel für Production. Stattdessen als Vitest-Test über die Sample-Responses aus den Handler-Unit-Tests:
// vitest test
import { validateAgainstSchema } from '../test-utils';
it('returns schema-conforming user', async () => {
const res = await request(app).get('/users/123');
expect(res.body).toBe(validateAgainstSchema('users.response.schema.json'));
});
Schema-Drift wird bei jedem Test-Run sofort entdeckt.
Ergebnis nach 6 Monaten
- 0 Production-Bugs durch falsche Payload-Struktur
- 0 Schema-Drift-Incidents (Schema und Implementation auseinanderlaufen)
- Pro Endpoint nur ~10 Minuten Onboarding-Zeit für neue Devs (Schema lesen statt Code lesen)
- OpenAPI-Spec automatisch aus den Schemas generiert