Skip to main content

API Contract Governance, Linting & Testing

Once more than one team consumes an API, "documentation" is not enough - you need automated enforcement of the contract.

1. Pipeline

Design-First API Pipeline (click a stage)

DesignOpenAPI 3.1step 1LintSpectralstep 2Diffoasdiffstep 3MockPrismstep 4CodegenOpenAPI Generatorstep 5Contract TestPact / Schemathesisstep 6PublishPortal / Registrystep 7Every stage is a CI gate: a failing stage blocks merge

Design Β· OpenAPI 3.1

The spec is the contract and source of truth. Model resources, schemas, errors, security schemes, and examples BEFORE writing code. Consumers review via pull request.

Gate: Review by API guild + consumers

openapi.yaml (paths, components.schemas, securitySchemes)

2. Design-First vs Code-First vs Hybrid

DimensionDesign-firstCode-firstHybrid (recommended for platforms)
Source of truthHand-written OpenAPIAnnotations/codeSpec for new/changed APIs; generated spec snapshot for legacy
ReviewContract reviewed by consumers before codingAfter implementationBoth
DriftPrevented (generated interfaces + tests)Likely without snapshot testsControlled by CI diff
ToolingOpenAPI Generator, Prism, Spectralspringdoc, FastAPI, NestJSBoth
WeaknessSpec writing skill; YAML noiseAccidental breaking changes, poor descriptionsProcess discipline

Choose by blast radius: public/partner/cross-team APIs -> design-first; single-team internal service -> code-first with a committed spec snapshot.

3. Spec Layout & Reuse

api/
openapi.yaml # root: info, servers, paths via $ref, security
paths/orders.yaml
components/
schemas/Order.yaml
parameters/Pagination.yaml
responses/Errors.yaml
.spectral.yaml
CHANGELOG.md # generated from oasdiff

Bundle for tooling: redocly bundle api/openapi.yaml -o dist/openapi.yaml. Share common components (Problem, pagination, security) via a shared library repo referenced by version tag so all APIs stay consistent.

4. Linting with Spectral

spectral lint api/openapi.yaml --ruleset .spectral.yaml --format junit --fail-severity error

Rule categories to enforce (see ruleset in Naming Conventions):

CategoryExamples
Namingkebab paths, camelCase properties, operationId camelCase
Completenessdescription on every operation/parameter, examples present, tags set, servers defined
SafetymaxLength/maxItems/maximum on inputs, security defined (global or per op), no http:// servers
Consistency4xx/5xx use application/problem+json, pagination params standard, Idempotency-Key on unsafe POSTs
Hygieneunused components, duplicate operationId, examples validate against schemas

5. Breaking-Change Detection

git show origin/main:api/openapi.yaml > /tmp/base.yaml
oasdiff breaking /tmp/base.yaml api/openapi.yaml --fail-on ERR # fails CI on breaking changes
oasdiff changelog /tmp/base.yaml api/openapi.yaml --format markdown # PR comment / CHANGELOG

Escape hatch: breaking changes require a major version bump or an explicit, time-boxed waiver label reviewed by the API guild. Remember tools only catch structural breaks - semantic changes (meaning, default sort, rounding) need human review.

6. Mocking for Parallel Work

prism mock api/openapi.yaml -p 4010 --dynamic # random valid data from schemas
prism proxy api/openapi.yaml https://staging.example.com --errors # validates real traffic vs the contract

Good examples in the spec make mocks useful (return realistic values; include edge cases like empty lists and error bodies).

7. Server Stubs & Client SDKs

# Spring server interfaces (controllers implement generated interfaces; the compiler enforces the contract)
openapi-generator-cli generate -g spring -i api/openapi.yaml -o build/generated \
--additional-properties=interfaceOnly=true,useSpringBoot3=true,useTags=true,documentationProvider=none,openApiNullable=false

# Typed TypeScript client
openapi-generator-cli generate -g typescript-fetch -i api/openapi.yaml -o clients/ts
// Generated: interface OrdersApi { ResponseEntity<Order> getOrderById(String orderId); ... }
@RestController
public class OrdersController implements OrdersApi {
@Override
public ResponseEntity<Order> getOrderById(String orderId) { /* implement */ return null; }
}

Pitfalls: generated code is not a place for hand edits; commit templates/config not output (or publish SDKs as artifacts); pin generator versions (output changes between releases); review naming of generated models (oneOf quirks).

8. Testing the Contract

LayerToolWhat it proves
Provider conformanceSchemathesis, Dredd, RestAssured + openapi-validatorReal responses match the schema/status codes; no 500s on valid/invalid input (property-based fuzzing)
Consumer-driven contractsPact (broker + can-i-deploy)Provider changes don't break what consumers actually use
Schema validation in unit testsAtlassian swagger-request-validatorController IO validated against spec in MockMvc tests
CompatibilityoasdiffSpec-to-spec backward compatibility
SecurityOWASP ZAP/API scanning with the spec as input, Schemathesis auth testsBroken auth, injection, BOLA candidates
Performancek6/Gatling generated from the specSLO regression
schemathesis run api/openapi.yaml --base-url http://localhost:8080 \
--checks all --hypothesis-max-examples 200 --header "Authorization: Bearer $TOKEN"
// Response validation in a Spring test with swagger-request-validator-mockmvc
private final OpenApiValidationMatchers openApi = OpenApiValidationMatchers.openApi().isValid("api/openapi.yaml");

@Test
void getOrderMatchesContract() throws Exception {
mvc.perform(get("/api/v1/orders/ord_1")).andExpect(status().isOk()).andExpect(openApi.isValid("api/openapi.yaml"));
}

Pact in 5 Lines of Theory

Consumer tests record "when I call X I expect Y" into a pact file -> published to a broker -> provider CI replays pacts against the real service -> can-i-deploy gates release if any consumer pact would break. It captures actual usage, so providers may freely change what no consumer uses.

9. CI Pipeline Example (GitHub Actions)

name: api-contract
on: [pull_request]
jobs:
contract:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
with: { fetch-depth: 0 }
- name: Lint
run: npx @stoplight/spectral-cli lint api/openapi.yaml --fail-severity error
- name: Breaking changes
run: |
git show origin/main:api/openapi.yaml > base.yaml
docker run --rm -v "$PWD":/w -w /w tufin/oasdiff breaking base.yaml api/openapi.yaml --fail-on ERR
- name: Generate server interfaces & compile
run: ./mvnw -q -Pgenerate-api verify
- name: Provider contract fuzz
run: |
./mvnw -q spring-boot:start
pipx run schemathesis run api/openapi.yaml --base-url http://localhost:8080 --checks all

10. Organisational Governance

PracticeDetail
API style guideOne document, versioned, enforced by Spectral; exceptions are explicit
API guild / review boardLightweight review for new public APIs (design review in PR, 48h SLA) - enable, don't gate
API catalogBackstage/Apicurio/Postman: owner, lifecycle (experimental -> beta -> stable -> deprecated), SLO, docs, spec link
Lifecycle labelsx-lifecycle, stability promises per label
OwnershipEvery API has a team, on-call, and a deprecation owner
TelemetryPer-endpoint/client usage, error rate, latency; drives deprecation decisions
Developer portalRedoc/Scalar docs, runnable examples, SDKs, changelog, status page
Security gatesSpec-driven authz review (scopes per operation), gateway policies generated from spec

11. Anti-Patterns

Anti-patternFix
Spec generated at runtime only; nobody reviews diffsCommit snapshot + CI diff
Spec lies (200 documented, 201 returned)Conformance fuzzing
One mega-spec for 40 servicesOne spec per deployable API; shared components repo
Governance by committee that blocks deliveryAutomated rules + self-service + opt-in review
SDK generation copied into repo and editedRegenerate in CI; extend via wrappers
Mocks drifting from realityPrism proxy/validation mode in staging

Next: API Security Checklist (OWASP API Top 10).

πŸ“–
Track Page Progress0 / 635 Read
Knowledge Base Completion0%