Runtime OpenAPI Validation with express-openapi-validator

Runtime OpenAPI Validation with express-openapi-validator

Your OpenAPI spec describes what your API should accept and return. But without enforcement, it's just documentation — developers drift, inputs go unvalidated, and responses gradually diverge from what consumers expect.

express-openapi-validator is middleware that makes your OpenAPI spec executable. It validates every incoming request and outgoing response against your spec at runtime, turning your documentation into a live contract.

What It Does

When you mount the middleware, it intercepts every request before it reaches your route handlers and validates:

  • Request body — correct shape, required fields, correct types
  • Query parameters — allowed params, correct types, required params present
  • Path parameters — type coercion, format validation
  • Headers — required headers, correct formats
  • Response body (optional) — what you return matches what the spec promises

Violations throw errors with structured messages. You can handle them in your Express error handler and return proper 400/422 responses automatically — no manual validation code in route handlers.

Installation

npm install express-openapi-validator

Basic Setup

const express = require('express');
const OpenApiValidator = require('express-openapi-validator');
const path = require('path');

const app = express();
app.use(express.json());

app.use(
  OpenApiValidator.middleware({
    apiSpec: path.join(__dirname, 'openapi.yaml'),
    validateRequests: true,
    validateResponses: true,
  })
);

// Your routes
app.post('/users', (req, res) => {
  // req.body is already validated here
  res.status(201).json({ id: 1, ...req.body });
});

// Error handler for validation failures
app.use((err, req, res, next) => {
  res.status(err.status || 500).json({
    message: err.message,
    errors: err.errors,
  });
});

app.listen(3000);

With this in place, any request that doesn't match your spec gets rejected before it reaches your handler — no extra validation code needed.

OpenAPI Spec Example

openapi: '3.0.0'
info:
  title: User API
  version: '1.0.0'
paths:
  /users:
    post:
      operationId: createUser
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [name, email]
              properties:
                name:
                  type: string
                  minLength: 1
                  maxLength: 100
                email:
                  type: string
                  format: email
                age:
                  type: integer
                  minimum: 0
                  maximum: 150
      responses:
        '201':
          content:
            application/json:
              schema:
                type: object
                required: [id, name, email]
                properties:
                  id:
                    type: integer
                  name:
                    type: string
                  email:
                    type: string

If a POST arrives without name, with an invalid email format, or with age: -5, the middleware rejects it with a descriptive error before your handler runs.

Validation Error Format

Validation errors come back as structured objects you can return directly to clients:

{
  "message": "request.body.email must match format \"email\"",
  "errors": [
    {
      "path": ".body.email",
      "message": "must match format \"email\"",
      "errorCode": "format.openapi.validation"
    }
  ]
}

This is far better than letting invalid data reach your database layer or returning cryptic 500 errors.

Response Validation

Response validation is off by default because it has a performance cost — you're serializing responses to check them. But during development and testing it's invaluable. It catches:

  • Missing required fields in responses
  • Wrong types in what you return
  • Fields your spec doesn't mention (if additionalProperties: false)
  • Status codes your spec doesn't define
app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateRequests: true,
    validateResponses: {
      removeAdditional: 'failing', // strip undeclared fields
      onError: (error, body, req) => {
        console.error('Response validation failed:', error.message);
        // Don't throw in production — just log
      }
    },
  })
);

In production, set validateResponses to false or to a non-throwing mode. Use it in development and CI to catch drift between your implementation and spec.

Custom Formats

OpenAPI has built-in formats (email, date, uuid, etc.) but you can add your own:

const OpenApiValidator = require('express-openapi-validator');

app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateRequests: true,
    formats: {
      'phone-number': {
        type: 'string',
        validate: (value) => /^\+?[1-9]\d{1,14}$/.test(value),
      },
      'slug': {
        type: 'string',
        validate: (value) => /^[a-z0-9-]+$/.test(value),
      }
    }
  })
);

Now you can use format: phone-number in your spec and get validation for free.

File Uploads

The middleware handles multipart form data:

requestBody:
  content:
    multipart/form-data:
      schema:
        type: object
        required: [file]
        properties:
          file:
            type: string
            format: binary
          description:
            type: string
const multer = require('multer');
const upload = multer({ dest: 'uploads/' });

app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateRequests: true,
    fileUploader: { dest: 'uploads/' }
  })
);

Security Scheme Validation

Define security schemes in your spec and the middleware can enforce them:

components:
  securitySchemes:
    bearerAuth:
      type: http
      scheme: bearer
      bearerFormat: JWT
app.use(
  OpenApiValidator.middleware({
    apiSpec: './openapi.yaml',
    validateSecurity: {
      handlers: {
        bearerAuth: async (req, scopes, schema) => {
          const token = req.headers.authorization?.split(' ')[1];
          if (!token) throw new Error('Missing token');
          const user = await verifyJWT(token);
          req.user = user;
          return true;
        }
      }
    }
  })
);

Testing with the Validator Active

Keep the validator in your test environment. This ensures tests written against the real API surface also validate the contract:

// test/app.test.js
const request = require('supertest');
const app = require('../app'); // validator is mounted

test('POST /users rejects missing email', async () => {
  const res = await request(app)
    .post('/users')
    .send({ name: 'Alice' });

  expect(res.status).toBe(400);
  expect(res.body.errors[0].path).toBe('.body.email');
});

test('POST /users accepts valid input', async () => {
  const res = await request(app)
    .post('/users')
    .send({ name: 'Alice', email: 'alice@example.com' });

  expect(res.status).toBe(201);
  expect(res.body).toHaveProperty('id');
});

These tests verify both your validation logic and your response contract in one pass.

Keeping Spec and Code in Sync

The middleware keeps your server honest against the spec at runtime, but you also need confidence that your running API behaves correctly over time. HelpMeTest monitors your API endpoints continuously in production — running real scenarios 24/7 and alerting you when responses drift from expected behavior. Runtime validation catches malformed requests; continuous monitoring catches behavioral regressions after deploy.

Read more

Start now free