json-validator.de

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

SchichtWas validiert wirdWann
Build-Step (CI)Schemas selbst (sind sie valides JSON Schema?)Pre-Push, Pre-Deploy
Ajv pre-compileSchemas → compile()-FunktionenApp-Start
Request-MiddlewareRequest-Body gegen request.schemaPro Request, vor Handler
Response (Tests)Sample-Responses gegen response.schemaVitest, 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