Dredd: Contract Testing for API Blueprint and OpenAPI
API documentation that lies is worse than no documentation. When a spec says an endpoint returns { "id": "string" } but the server actually returns { "id": 42 }, every consumer who read the docs gets a runtime surprise.
Dredd is a tool specifically built to prevent this. It reads your API description document — API Blueprint or OpenAPI — and actually sends the described requests to your running server, comparing the responses against what the spec promises. If the spec says 201, Dredd checks for 201. If the spec says the response body has a name field, Dredd verifies it exists.
This is contract testing: the spec is the contract, Dredd is the enforcement mechanism.
How Dredd Works
The core loop is simple:
- Dredd parses your API description (Blueprint
.apibfile or OpenAPI.yaml/.json) - For each transaction defined in the spec, it generates an HTTP request using the documented examples
- It sends that request to your running server
- It compares the actual response against the documented expected response
- It reports which transactions passed and which failed
The critical insight is that Dredd relies on examples in your spec. If your spec doesn't include request body examples or expected response examples, Dredd has less to work with. Good example coverage in your spec directly translates to better Dredd test coverage.
Installation
# Install globally
npm install -g dredd
# Or per-project
npm install --save-dev dredd
# Verify installation
dredd --versionDredd requires Node.js 12 or later.
Basic Usage with OpenAPI
Given an OpenAPI spec (openapi.yaml):
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/users/{userId}:
get:
summary: Get user by ID
parameters:
- name: userId
in: path
required: true
schema:
type: string
example: "user-123"
responses:
'200':
description: User found
content:
application/json:
schema:
$ref: '#/components/schemas/User'
example:
id: "user-123"
email: "alice@example.com"
name: "Alice"
createdAt: "2024-01-15T10:30:00Z"
'404':
description: User not found
content:
application/json:
example:
error: "USER_NOT_FOUND"
message: "User with id user-123 not found"
components:
schemas:
User:
type: object
required: [id, email, createdAt]
properties:
id:
type: string
email:
type: string
name:
type: string
createdAt:
type: string
format: date-timeRun Dredd against a local server:
dredd openapi.yaml http://localhost:3000Dredd will make a GET /users/user-123 request (using the example value from the spec) and verify the response.
Configuration File
For anything beyond the simplest setup, use a dredd.yml config file:
# dredd.yml
dry-run: false
hookfiles: ./dredd-hooks/*.js
language: nodejs
sandbox: false
server: npm start
server-wait: 5
init: false
custom: {}
names: false
only: []
reporter: cli
output: []
header: []
sorted: false
user: null
inline-errors: false
details: false
method: []
color: true
level: info
timestamp: false
silent: false
path: []
blueprint: openapi.yaml
endpoint: http://localhost:3000Key options:
server— command to start your API server before running testsserver-wait— seconds to wait after starting the serverhookfiles— path to hook scripts (more on this below)reporter— output format (cli,junit,xunit,dot,markdown,apiary)
Run with config:
dredd # Uses dredd.yml automaticallyAPI Blueprint Format
Dredd was originally built for API Blueprint, a Markdown-based API description format. While OpenAPI has become more common, Blueprint is still supported and has a distinctive readable syntax:
FORMAT: 1A
# User API
## Users [/users]
### Create User [POST]
Create a new user account.
+ Request (application/json)
+ Attributes
+ email: alice@example.com (string, required)
+ name: Alice Smith (string, required)
+ password: s3cr3t (string, required)
+ Body
{
"email": "alice@example.com",
"name": "Alice Smith",
"password": "s3cr3t"
}
+ Response 201 (application/json)
+ Body
{
"id": "user-123",
"email": "alice@example.com",
"name": "Alice Smith",
"createdAt": "2024-01-15T10:30:00Z"
}
## User [/users/{userId}]
+ Parameters
+ userId: `user-123` (string) - The user's unique ID
### Get User [GET]
+ Response 200 (application/json)
+ Body
{
"id": "user-123",
"email": "alice@example.com",
"name": "Alice Smith",
"createdAt": "2024-01-15T10:30:00Z"
}
+ Response 404 (application/json)
+ Body
{
"error": "USER_NOT_FOUND",
"message": "User with id user-123 not found"
}Hooks: Customizing Request Flow
Hooks are where Dredd becomes truly powerful. Many APIs require authentication, specific database state, or request modification that a static spec can't capture. Hooks let you inject JavaScript (or other languages) at specific points in the test lifecycle.
Hook lifecycle events:
beforeAll— runs once before all transactionsafterAll— runs once after all transactionsbefore("Transaction Name")— runs before a specific transactionafter("Transaction Name")— runs after a specific transactionbeforeEach— runs before every transactionafterEach— runs after every transaction
Authentication Hook
// dredd-hooks/auth.js
const hooks = require('hooks');
// Obtain a token once before all tests run
let authToken;
hooks.beforeAll(async function(transactions, done) {
try {
const response = await fetch('http://localhost:3000/auth/token', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
client_id: process.env.TEST_CLIENT_ID,
client_secret: process.env.TEST_CLIENT_SECRET,
grant_type: 'client_credentials'
})
});
const data = await response.json();
authToken = data.access_token;
console.log('Auth token obtained');
done();
} catch (err) {
done(err);
}
});
// Inject the token into every request
hooks.beforeEach(function(transaction) {
transaction.request.headers['Authorization'] = `Bearer ${authToken}`;
});Seeding and Cleanup Hooks
// dredd-hooks/setup.js
const hooks = require('hooks');
const db = require('../test-helpers/db');
let testUserId;
hooks.before('User > Create User > Create User', async function(transaction, done) {
// Nothing to set up — we're creating the resource
done();
});
hooks.after('User > Create User > Create User', async function(transaction, done) {
// Extract the created user ID for subsequent tests
const body = JSON.parse(transaction.real.body);
testUserId = body.id;
done();
});
hooks.before('User > User > Get User', function(transaction) {
// Inject the real user ID into the URL
transaction.fullPath = transaction.fullPath.replace('user-123', testUserId);
});
hooks.after('User > User > Get User', async function(transaction, done) {
// Clean up the test user
await db.query('DELETE FROM users WHERE id = $1', [testUserId]);
done();
});
// Skip a transaction that requires setup you haven't done
hooks.before('Admin > Delete All Users > Delete All Users', function(transaction) {
transaction.skip = true;
});Modifying Request Bodies
hooks.before('Order > Orders > Create Order', function(transaction) {
// Replace static example with dynamic values
const body = JSON.parse(transaction.request.body);
body.productId = process.env.TEST_PRODUCT_ID;
body.customerId = process.env.TEST_CUSTOMER_ID;
transaction.request.body = JSON.stringify(body);
transaction.request.headers['Content-Length'] =
Buffer.byteLength(transaction.request.body).toString();
});Skipping and Focusing Transactions
Not every spec transaction is testable in every environment. Dredd provides mechanisms to skip selectively:
# Skip specific transactions by name
dredd openapi.yaml http://localhost:3000 \
--skip-endpoints-without-servers \
--only "User > Get User"
# Run only transactions matching a path pattern
dredd openapi.yaml http://localhost:3000 --method GET
# Skip specific paths
dredd openapi.yaml http://localhost:3000 \
--path /admin # Skip admin endpointsIn hooks, you can skip conditionally:
hooks.before('Admin > Delete Database > Delete Database', function(transaction) {
if (process.env.CI === 'true') {
transaction.skip = true;
console.log('Skipping destructive test in CI');
}
});Reporters and Output
Dredd supports multiple output formats for different consumers:
# JUnit XML (for CI systems)
dredd openapi.yaml http://localhost:3000 \
--reporter junit \
--output results.xml
# Multiple reporters simultaneously
dredd openapi.yaml http://localhost:3000 \
--reporter junit \
--output results/junit.xml \
--reporter cli
# Markdown report
dredd openapi.yaml http://localhost:3000 \
--reporter markdown \
--output results/report.mdCI/CD Integration
GitHub Actions
name: API Contract Tests
on:
push:
branches: [main, develop]
pull_request:
paths:
- 'src/**'
- 'openapi.yaml'
jobs:
contract-tests:
runs-on: ubuntu-latest
services:
postgres:
image: postgres:15
env:
POSTGRES_DB: testdb
POSTGRES_USER: test
POSTGRES_PASSWORD: test
options: >-
--health-cmd pg_isready
--health-interval 10s
--health-retries 5
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '20'
cache: 'npm'
- name: Install dependencies
run: npm ci
- name: Run database migrations
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
run: npm run db:migrate
- name: Seed test data
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
run: npm run db:seed:test
- name: Run contract tests
env:
DATABASE_URL: postgresql://test:test@localhost:5432/testdb
TEST_CLIENT_ID: ${{ secrets.TEST_CLIENT_ID }}
TEST_CLIENT_SECRET: ${{ secrets.TEST_CLIENT_SECRET }}
run: |
npm run start:test &
npx wait-on http://localhost:3000/health
npx dredd --reporter junit --output contract-results.xml
- name: Publish contract test results
uses: mikepenz/action-junit-report@v4
if: always()
with:
report_paths: contract-results.xml
check_name: Contract TestsPackage.json Scripts
{
"scripts": {
"test:contract": "dredd",
"test:contract:ci": "dredd --reporter junit --output results/contract.xml",
"test:contract:verbose": "dredd --level debug"
}
}Handling Common Issues
Problem: Dredd uses example values that don't exist in your database
Solution: Use before hooks to create the required resources, then patch the transaction URL to use real IDs:
let realUserId;
hooks.beforeAll(async (transactions, done) => {
const resp = await createTestUser({ email: 'dredd-test@example.com' });
realUserId = resp.id;
done();
});
hooks.beforeEach(function(transaction) {
// Replace all occurrences of the spec's example ID
transaction.fullPath = transaction.fullPath.replace(/user-123/g, realUserId);
if (transaction.request.body) {
transaction.request.body = transaction.request.body.replace(
/user-123/g,
realUserId
);
}
});Problem: Spec has many 4xx examples that need specific conditions to trigger
Solution: Skip them in normal runs, test them explicitly with dedicated unit/integration tests:
hooks.beforeEach(function(transaction) {
const status = parseInt(transaction.expected.statusCode);
if (status >= 400 && status < 500) {
// Only run error cases if explicitly opted in
if (!process.env.TEST_ERROR_CASES) {
transaction.skip = true;
}
}
});Limitations to Know
Dredd validates structure, not business logic. It checks that the response matches the shape described in your spec. It won't verify that a GET /users response actually reflects database state, or that creating an order actually reduces inventory.
Request/response examples must exist in your spec. Dredd can only test what's documented with examples. Schemas without examples are tested minimally.
Authentication flows need hooks. If your API requires session cookies, CSRF tokens, or multi-step auth, hooks are the only way to handle this.
Dredd is maintained but not rapidly evolving. The project has slowed down — GitHub activity is low. It still works well, but don't expect new features. For actively-developed alternatives, look at Schemathesis (property-based, more aggressive testing) or Prism (mock server + validation).
Summary
Dredd occupies a specific and useful niche: automated enforcement that your running server matches your published spec. This is contract testing at the HTTP layer — fast, runnable in CI, and requiring zero test code for the baseline case (just a spec with good examples).
The hook system makes it practical for real APIs that need auth and database state. The JUnit reporter makes it a natural fit for existing CI pipelines. And the very act of writing Dredd-compatible specs forces you to include concrete examples, which makes your documentation more useful regardless of whether Dredd is in your pipeline.
If you're already maintaining an OpenAPI spec, adding Dredd to CI is low-effort insurance against the spec diverging from reality.