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 dreddVerify:
dredd --versionTesting 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 foundRun Dredd against your local server:
dredd openapi.yml http://localhost:3000Dredd 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:3000Dredd 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.htmlRun with config:
dreddHooks 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:3000For HTML reports in CI:
dredd openapi.yml http://localhost:3000 \
--reporter html \
--output dredd-report.htmlCommon 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.