From 01dac5f7013dcc02a27596fe00bf2fad7ac5166e Mon Sep 17 00:00:00 2001 From: laxjovial Date: Tue, 30 Jun 2026 07:59:45 +0100 Subject: [PATCH] feat: add OpenAPI schema validation middleware and fix mismatches (fixes #842) --- package.json | 1 + src/app.controller.ts | 6 +++++- src/main.ts | 36 ++++++++++++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/package.json b/package.json index 613e45a8..61dd1002 100644 --- a/package.json +++ b/package.json @@ -135,6 +135,7 @@ "csurf": "^1.2.2", "dataloader": "^2.2.3", "express": "^5.2.1", + "express-openapi-validator": "^5.3.9", "express-session": "^1.19.0", "fast-xml-parser": "^5.2.5", "fluent-ffmpeg": "^2.1.3", diff --git a/src/app.controller.ts b/src/app.controller.ts index 8ed9f082..367c8511 100644 --- a/src/app.controller.ts +++ b/src/app.controller.ts @@ -19,6 +19,10 @@ export class AppController { }, }) getStatus() { - return { message: 'TeachLink API is running', timestamp: new Date().toISOString() }; + return { + success: true, + message: 'TeachLink API is running', + data: { timestamp: new Date().toISOString() } + }; } } diff --git a/src/main.ts b/src/main.ts index ad3bdec5..a993801c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -9,6 +9,8 @@ import { RedisStore } from 'connect-redis'; import Redis from 'ioredis'; import { AppModule } from './app.module'; +import * as OpenApiValidator from 'express-openapi-validator'; +import { join } from 'path'; import './tracing/opentelemetry'; import { CorrelationIdMiddleware } from './middleware/correlation-id'; @@ -311,6 +313,40 @@ async function bootstrapWorker(): Promise { // ========================= app.useGlobalInterceptors(new LocaleInterceptor(), new PaginationInterceptor()); + // ========================= + // OPENAPI VALIDATION + // ========================= + const apiSpecPath = join(process.cwd(), 'docs/api/openapi-spec.json'); + app.use( + OpenApiValidator.middleware({ + apiSpec: apiSpecPath, + validateRequests: true, + validateResponses: process.env.NODE_ENV !== 'production', + ignorePaths: /.*\/api\/docs.*/, // ignore swagger docs + }), + ); + + app.use((err: any, req: Request, res: Response, next: NextFunction) => { + if (err.status === 400 && err.errors) { + return res.status(400).json({ + success: false, + message: 'Validation failed', + errors: err.errors.map((e: any) => ({ + field: e.path, + message: e.message, + })), + }); + } + if (err.status === 500 && err.errors && typeof err.message === 'string' && err.message.toLowerCase().includes('response')) { + logger.warn(`Response validation deviation: ${JSON.stringify(err.errors)}`); + return res.status(500).json({ + success: false, + message: 'Internal server error', + }); + } + next(err); + }); + // ========================= // SWAGGER // =========================