Build APIs that last. Covers OpenAPI spec design, versioning strategies, authentication patterns, rate limiting, and contract testing for enterprise APIs.
API-first architecture is a disciplined approach where the API contract defines the system, not the implementation. It matters when multiple teams or services must interact reliably, when external developers consume your system, or when you are building a distributed application where consistency, scalability, and maintainability are non-negotiable.
Begin with an API specification document that is the single source of truth. The contract must be machine-readable and versioned, and all developers—frontend, backend, mobile, third-party—use it to build, test, and validate their integration.
Use OpenAPI (formerly Swagger) as the standard format. Define every endpoint, request and response schema, error codes, headers, and security schemes. The specification file (openapi.yaml) is the foundation.
openapi: 3.0.3
info:
title: User Management API
version: 1.0.0
description: Manage users, roles, and authentication
servers:
- url: https://api.example.com/v1
paths:
/users:
get:
summary: List all users
parameters:
- name: page
in: query
schema:
type: integer
minimum: 1
default: 1
- name: per_page
in: query
schema:
type: integer
minimum: 1
maximum: 100
default: 20
responses:
'200':
description: A list of users
content:
application/json:
schema:
$ref: '#/components/schemas/UserList'
'400':
description: Invalid query parameters
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
components:
schemas:
UserList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
pagination:
$ref: '#/components/schemas/Pagination'
User:
type: object
required:
- id
- email
- created_at
properties:
id:
type: string
format: uuid
email:
type: string
format: email
name:
type: string
roles:
type: array
items:
type: string
created_at:
type: string
format: date-time
Pagination:
type: object
properties:
page:
type: integer
per_page:
type: integer
total:
type: integer
Error:
type: object
required:
- code
- message
properties:
code:
type: string
example: VALIDATION_ERROR
message:
type: string
details:
type: array
items:
type: object
properties:
field:
type: string
message:
type: string
Validate the OpenAPI schema using openapi-lint:
npm install -g openapi-lint
openapi-lint openapi.yaml
A failed validation outputs:
openapi.yaml:23:12 - error: expected 'parameters' to be an array
openapi.yaml:42:8 - warning: 'page' parameter missing 'required' field
Every new API endpoint must be documented in this file before any code is written. The contract drives design reviews and is the basis for auto-generated client libraries, SDKs, and documentation.
Versioning is not optional—it is a core discipline. Use URL path-based versioning for clarity and independence.
All endpoints are prefixed with /v1, /v2, etc. Versioning allows backward compatibility while evolving the API.
The Accept header governs content negotiation:
GET /v1/users?page=1&per_page=10 HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.user-management.v1+json
The server responds with:
HTTP/1.1 200 OK
Content-Type: application/vnd.example.user-management.v1+json
{
"data": [
{
"id": "a1b2c3d4-e5f6-7890-g1h2-i3j4k5l6m7n8",
"email": "[email protected]",
"name": "Alice Johnson",
"roles": ["admin", "editor"],
"created_at": "2023-10-05T12:34:56Z"
}
],
"pagination": {
"page": 1,
"per_page": 10,
"total": 42
}
}
When introducing a breaking change—such as renaming email to contact.email—create a new version: /v2/users.
The Accept header must include the versioned media type:
Accept: application/vnd.example.user-management.v2+json
The server selects the version based on the Accept header, falling back to the path version if needed. Clients can upgrade at their own pace.
Use a versioning policy: minor versions (v1.1, v1.2) for non-breaking changes; major versions (v2, v3) for breaking changes. Versioning is not tied to a release cycle but to the evolution of the contract.
Testing begins at the contract level. No integration test is valid without first validating the OpenAPI specification.
Use swagger-cli to generate test cases from the specification:
npm install -g swagger-cli
swagger validate openapi.yaml
swagger test openapi.yaml --server https://api.example.com
This runs a suite of tests against the actual API endpoint, validating:
Error schemaA test failure outputs:
POST /v1/users: 400 Bad Request
expected: application/vnd.example.user-management.v1+json
actual: application/json
POST /v1/users: body validation failed
missing required property: name
invalid type for: roles, expected array, got object
Create a test suite in test/api-contract.test.js using jest and supertest:
const request = require('supertest');
const app = require('../src/server');
describe('User Management API', () => {
test('GET /v1/users returns paginated list', async () => {
const res = await request(app)
.get('/v1/users')
.query({ page: 1, per_page: 5 })
.expect(200);
expect(res.headers['content-type']).toMatch(/application\/vnd\.example\.user-management\.v1\+json/);
expect(res.body.data).toBeInstanceOf(Array);
expect(res.body.data.length).toBeLessThanOrEqual(5);
expect(res.body.pagination).toHaveProperty('page', 1);
expect(res.body.pagination).toHaveProperty('per_page', 5);
});
test('POST /v1/users creates a new user', async () => {
const res = await request(app)
.post('/v1/users')
.send({
email: '[email protected]',
name: 'Bob Smith'
})
.expect(201);
expect(res.body).toHaveProperty('id');
expect(res.body.email).toBe('[email protected]');
expect(res.body.name).toBe('Bob Smith');
expect(res.headers.location).toBe(`/v1/users/${res.body.id}`);
});
});
Run tests with:
npm test
The test suite is the first line of defense: if it fails, the API contract is broken.
Integrate API contract validation into CI. Every pull request must validate the OpenAPI spec.
In a GitHub Actions workflow:
name: API Contract Tests
on: [pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Install dependencies
run: npm install
- name: Validate OpenAPI spec
run: openapi-lint openapi.yaml
- name: Run contract tests
run: npm test
- name: Deploy to staging
run: |
npm run deploy:staging
curl -X POST https://api.example.com/v1/deployments \
-H "Authorization: Bearer $DEPLOY_TOKEN" \
-d '{"branch": "main", "sha": "abc123"}'
The openapi-lint and test steps fail the PR if the contract is inconsistent or if the implementation deviates from the specification.
Use curl to manually test endpoints in development and staging:
curl -X GET https://api.example.com/v1/users \
-H "Accept: application/vnd.example.user-management.v1+json" \
-H "Authorization: Bearer abc123" \
-v
When a new version is released, ensure clients can adapt gradually.
Introduce a Prefer header to guide clients:
GET /v1/users HTTP/1.1
Host: api.example.com
Accept: application/vnd.example.user-management.v1+json
Prefer: return=representation; include=next
The server responds with a Link header to help clients navigate:
Link: <https://api.example.com/v1/users?page=2>; rel="next",
<https://api.example.com/v1/users?page=1>; rel="prev",
<https://api.example.com/v1/users?page=3>; rel="last"
Clients can follow next links automatically.
Use a deprecation policy: announce breaking changes in a Deprecation header:
Deprecation: true; until="2024-06-01"
Clients that use the older version receive a Warning header:
Warning: 299 "Deprecated API" "The /v1/users endpoint will be removed in v3.0.0"
This allows teams to plan upgrades without breaking their users.
Content-Type in POST/PUT: Clients send application/json, but the server expects application/vnd.example.user-management.v1+json. The response is 400 with {"code": "MEDIA_TYPE_NOT_SUPPORTED", "message": "Expected application/vnd.example.user-management.v1+json"}.Accept header: Clients send application/json, but the server returns application/vnd.example.user-management.v1+json. The client parses the response but fails to understand the roles field.Accept: A client sends application/vnd.example.user-management.v2+json, but the server only supports v1. The server returns v1 format, but the client expects v2.400 response includes details field, but the client expects errors instead.Location header on POST: Clients expect Location: /v1/users/uuid, but the server sends location: /v1/users/uuid (lowercase).total field missing in pagination object, causing clients to incorrectly estimate total results.API-first architecture succeeds when the contract is not just a document, but a living, tested, versioned system. Design begins with OpenAPI. Versioning uses path and media type. Testing validates the contract against real implementations. CI enforces adherence. Breaking changes are communicated via headers and warnings. Every detail—headers, status codes, error shapes, schema validation—is precise and consistent. This discipline reduces integration debt, accelerates team velocity, and enables independent deployment across services.
This page was rewritten on 15 October 2026. It replaced a templated version that was not specific to its own title. How these pages are written and checked. If anything here is wrong, tell us at [email protected] and we will correct it.
We use cookies for analytics (Google Analytics) and advertising (Google AdSense) to improve your experience and support free content. Privacy Policy