Dredd: API Blueprint and OpenAPI Testing Guide

Dredd: API Blueprint and OpenAPI Testing Guide

Dredd validates that your API implementation matches its documentation. It reads your API Blueprint or OpenAPI specification, makes real HTTP requests to your running server, and checks that the responses match what the spec promises. When your code and your docs disagree, Dredd fails — forcing you to keep them in sync.

The Core Problem Dredd Solves

API documentation lies. Not intentionally — but specs get updated, code gets changed, and the two drift apart. You end up with an OpenAPI file that says status is an integer, while your code started returning a string three months ago. Nobody noticed because all the integration tests were written before the drift.

Dredd turns your spec into an executable test suite. Every endpoint documented in your spec gets tested against your running server. If the behavior doesn't match, the build fails.

Installation

Dredd requires Node.js:

npm install -g dredd

Verify:

dredd --version

Testing an OpenAPI API

Create a minimal OpenAPI spec if you don't have one:

# openapi.yml
openapi: "3.0.0"
info:
  title: Example API
  version: "1.0"
paths:
  /users:
    get:
      summary: List users
      responses:
        "200":
          description: User list
          content:
            application/json:
              schema:
                type: array
                items:
                  type: object
                  properties:
                    id:
                      type: integer
                    name:
                      type: string
  /users/{id}:
    get:
      summary: Get user by ID
      parameters:
        - name: id
          in: path
          required: true
          schema:
            type: integer
      responses:
        "200":
          description: User
          content:
            application/json:
              schema:
                type: object
                properties:
                  id:
                    type: integer
                  name:
                    type: string
        "404":
          description: Not found

Run Dredd against your local server:

dredd openapi.yml http://localhost:3000

Dredd makes GET requests to /users and /users/{id} (with a generated ID), then validates the response structure against the schema.

API Blueprint Format

Dredd was originally built for API Blueprint, a Markdown-based API description format:

# API Blueprint Example

FORMAT: 1A
HOST: http://localhost:3000

# Group Users

## Users Collection [/users]

### List Users [GET]

+ Response 200 (application/json)

    + Body

            [
                {
                    "id": 1,
                    "name": "Alice"
                }
            ]

## User [/users/{id}]

+ Parameters
    + id (number) - User ID

### Get User [GET]

+ Response 200 (application/json)

    + Body

            {
                "id": 1,
                "name": "Alice"
            }

+ Response 404 (application/json)

    + Body

            {
                "message": "User not found"
            }

Run against your API:

dredd api.apib http://localhost:3000

Dredd Configuration File

For teams, use a configuration file instead of command-line flags:

# dredd.yml
dry-run: false
hookfiles:
  - ./hooks.js
language: nodejs
server: node server.js
server-wait: 3
endpoint: "http://localhost:3000"
path:
  - ./openapi.yml
reporter:
  - dot
  - html
output:
  - ./dredd-report.html

Run with config:

dredd

Hooks for Authentication

Many APIs require authentication. Dredd's hooks system lets you inject tokens before requests:

// hooks.js
const hooks = require('hooks');

// Before every request
hooks.beforeAll(function(transactions, done) {
  for (let transaction of transactions) {
    transaction.request.headers['Authorization'] = 'Bearer test-token-123';
  }
  done();
});

// Before a specific endpoint
hooks.before('Users Collection > List Users > GET', function(transaction, done) {
  transaction.request.headers['Authorization'] = 'Bearer admin-token';
  done();
});

For dynamic tokens (e.g., login first, use returned token):

const hooks = require('hooks');
const fetch = require('node-fetch');

let authToken;

hooks.beforeAll(async function(transactions, done) {
  // Authenticate and get token
  const response = await fetch('http://localhost:3000/auth/login', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ username: 'test', password: 'password' })
  });
  const data = await response.json();
  authToken = data.token;

  // Inject token into all transactions
  for (let transaction of transactions) {
    transaction.request.headers['Authorization'] = `Bearer ${authToken}`;
  }
  done();
});

Manipulating Request Bodies

For POST and PUT endpoints, Dredd uses example values from your spec. You can override them:

const hooks = require('hooks');

hooks.before('Users Collection > Create User > POST', function(transaction, done) {
  const body = {
    name: 'Test User',
    email: `test-${Date.now()}@example.com`,
    role: 'user'
  };
  transaction.request.body = JSON.stringify(body);
  done();
});

Skipping Problematic Endpoints

If certain endpoints aren't testable in your CI environment (e.g., payment endpoints that hit Stripe):

const hooks = require('hooks');

hooks.before('Payments > Create Payment > POST', function(transaction, done) {
  transaction.skip = true;
  done();
});

Or mark destructive operations that would break subsequent tests:

hooks.before('Users > Delete User > DELETE', function(transaction, done) {
  // Only run in dedicated cleanup tests
  if (process.env.CI) {
    transaction.skip = true;
  }
  done();
});

Storing State Between Requests

Dredd runs transactions sequentially. You can store state from earlier responses for use in later requests:

const hooks = require('hooks');
const stash = {};

// Create a user and save the ID
hooks.after('Users Collection > Create User > POST', function(transaction, done) {
  const responseBody = JSON.parse(transaction.real.body);
  stash.userId = responseBody.id;
  done();
});

// Use the saved ID for the get-by-ID request
hooks.before('User > Get User > GET', function(transaction, done) {
  const url = transaction.request.uri;
  transaction.request.uri = url.replace('/users/1', `/users/${stash.userId}`);
  done();
});

Reading Error Reports

When Dredd finds a mismatch:

fail: GET /users duration: 45ms
    request:
        method: GET
        uri: /users
        headers:
            Accept: application/json
    expected:
        headers:
            Content-Type: application/json
        body:
            [{"id": 1, "name": "Alice"}]
        statusCode: 200
    actual:
        statusCode: 200
        headers:
            Content-Type: application/json; charset=utf-8
        body:
            [{"id": 1, "name": "Alice", "created_at": "2024-01-01"}]
    request duration: 12ms

Message: Real and expected data does not match.

The expected section comes from your spec. The actual section is what your server returned. Here, the server returns a created_at field not documented in the spec — either add it to the spec or remove it from the response.

CI/CD Integration

GitHub Actions example:

name: API Contract Tests
on: [push, pull_request]

jobs:
  dredd:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - name: Use Node.js
        uses: actions/setup-node@v3
        with:
          node-version: '18'
      - name: Install dependencies
        run: npm ci
      - name: Install Dredd
        run: npm install -g dredd
      - name: Start API server
        run: node server.js &
      - name: Wait for server
        run: |
          until curl -f http://localhost:3000/health; do sleep 1; done
      - name: Run Dredd
        run: dredd openapi.yml http://localhost:3000

For HTML reports in CI:

dredd openapi.yml http://localhost:3000 \
  --reporter html \
  --output dredd-report.html

Common Issues and Fixes

Problem: Dredd tests paths with generated example values like /users/1 but that ID doesn't exist in your test database.

Fix: Use hooks to create the resource first and store its ID:

hooks.beforeAll(async function(transactions, done) {
  // Seed test data
  const response = await fetch('http://localhost:3000/users', {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ name: 'Test User' })
  });
  const user = await response.json();
  
  // Update all user-specific transactions with the real ID
  transactions
    .filter(t => t.request.uri.includes('/users/'))
    .forEach(t => {
      t.request.uri = t.request.uri.replace(/\/users\/\d+/, `/users/${user.id}`);
    });
  
  done();
});

Problem: Response has extra fields not in the spec.

Fix: Add those fields to your spec, or use strict mode to decide which side is authoritative. Typically you want to add undocumented fields to the spec — if your API returns them, document them.

Problem: Authentication errors on all requests.

Fix: Ensure your beforeAll hook is setting auth correctly. Log the transactions to verify:

hooks.beforeAll(function(transactions, done) {
  console.log('Setting auth for', transactions.length, 'transactions');
  // ... set auth
  done();
});

When to Use Dredd vs Schemathesis

Dredd validates that your implementation matches your docs — it uses the example values from your spec and checks conformance. This is spec-driven contract testing.

Schemathesis generates novel inputs from your schema to find unexpected behaviors — it explores edge cases your examples didn't cover. This is property-based fuzzing.

They're complementary: Dredd catches "the code doesn't match the spec" and Schemathesis catches "the spec allows inputs the code can't handle." Using both gives you comprehensive API quality coverage.

Dredd is particularly effective in teams where the API spec is the source of truth — when designers write the OpenAPI file first and developers implement against it. Every CI run verifies the implementation keeps pace with the spec.

Read more