Skip to main content
Welcome. This site supports keyboard navigation and screen readers. Press ? at any time for keyboard shortcuts. Press [ to focus the sidebar, ] to focus the content. High-contrast themes are available via the toolbar.
serard@dev00:~/cv

Part VI: API Specification — OpenAPI, AsyncAPI, Contract Testing

The OpenAPI spec guarantees that POST /users/{id}/roles accepts a Role object and returns a 204. It says nothing about whether an admin can actually assign roles, whether role changes take effect immediately, or whether non-admins are blocked. API shape is not feature behavior.

What They Are

API specification tools define the shape of an API — endpoints, request/response schemas, status codes, authentication requirements — in a machine-readable format:

  • OpenAPI (formerly Swagger): REST API specification in YAML or JSON
  • AsyncAPI: Event-driven / message-based API specification
  • Pact: Consumer-driven contract testing for microservice boundaries
  • GraphQL SDL: Schema Definition Language for GraphQL APIs
  • gRPC / Protocol Buffers: Strongly typed RPC interface definitions

Contract testing tools then verify that the implementation matches the specification.

The specification document IS the requirement. Tests validate that the implementation conforms:

# openapi.yaml — the specification
openapi: 3.0.3
info:
  title: User Roles API
  version: 1.0.0
paths:
  /users/{userId}/roles:
    post:
      operationId: assignRole
      summary: Assign a role to a user
      parameters:
        - name: userId
          in: path
          required: true
          schema:
            type: string
            format: uuid
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/RoleAssignment'
      responses:
        '204':
          description: Role assigned successfully
        '403':
          description: Caller lacks permission

components:
  schemas:
    RoleAssignment:
      type: object
      required: [role]
      properties:
        role:
          type: string
          enum: [viewer, editor, admin]
// Contract test — validates the implementation matches the spec
import { createValidator } from 'openapi-validator';

const validator = createValidator('./openapi.yaml');

test('POST /users/:id/roles matches the OpenAPI spec', async () => {
  const response = await api.post('/users/user-1/roles', {
    role: 'editor',
  });

  expect(response.status).toBe(204);

  // Validate the request/response pair against the spec
  const errors = validator.validate('POST', '/users/{userId}/roles', {
    request: { body: { role: 'editor' } },
    response: { status: 204 },
  });
  expect(errors).toHaveLength(0);
});

test('POST /users/:id/roles rejects invalid role', async () => {
  const response = await api.post('/users/user-1/roles', {
    role: 'superadmin',  // not in the enum
  });

  expect(response.status).toBe(400);
});

Pact takes a different approach — consumer-driven contracts:

// Consumer side (frontend)
const interaction = {
  uponReceiving: 'a request to assign a role',
  withRequest: {
    method: 'POST',
    path: '/users/user-1/roles',
    body: { role: 'editor' },
  },
  willRespondWith: {
    status: 204,
  },
};

// Provider side (backend) — verifies it satisfies the consumer's contract
// pact-verifier reads the contract and replays requests against the real API

What They Catch

API specification tools excel at shape validation at boundaries:

Schema drift. If the API returns a field that's not in the spec, or omits a required field, the contract test fails. This is particularly powerful in microservice architectures where services evolve independently.

Breaking changes. Schema diff tools (like oasdiff for OpenAPI) detect breaking changes between API versions: removed endpoints, changed field types, narrowed enums. These can be integrated into CI to prevent accidental breakage.

Consumer-provider mismatches. Pact-style contract testing catches cases where the frontend expects a field that the backend doesn't provide — without requiring end-to-end integration tests.

Documentation accuracy. The OpenAPI spec IS the documentation. Tools like Swagger UI and Redoc generate interactive API docs from the spec. If the spec is validated by tests, the documentation is always accurate.

Type generation. Tools like openapi-typescript, NSwag, and openapi-generator generate client SDKs and server stubs from the spec. The generated types enforce the contract at compile time in consuming code.

Where They Don't Apply

This is the critical distinction: API specs define what the API looks like, not what the feature does.

Shape vs. Behavior

The OpenAPI spec says POST /users/{id}/roles accepts a RoleAssignment and returns 204. It doesn't say:

  • Who can assign roles (authorization logic)
  • When role changes take effect (immediately? after a sync?)
  • What the user sees after the role change (UI feedback)
  • Whether the role change is audited (compliance requirement)
  • How concurrent role changes are handled (conflict resolution)

These are behavioral requirements — acceptance criteria that describe what the system should DO, not what it should LOOK LIKE. API specs can't express them.

Feature Scope vs. Endpoint Scope

A "User Roles" feature might have these ACs:

  1. Admin can assign roles to other users
  2. Non-admin cannot assign roles
  3. Role change takes effect immediately
  4. Role changes are logged in the audit trail
  5. Users see a confirmation after role assignment

AC #1 and #2 touch the API. AC #3 is a timing/consistency requirement. AC #4 is a backend/infrastructure concern. AC #5 is a frontend concern. The API spec covers 2 out of 5. The other 3 are invisible to contract testing.

Layer Coverage

Testable Behavior API Spec / Contract Typed Specs
Endpoint exists and accepts correct schema Yes No (not its job)
Response matches documented schema Yes No (not its job)
Authorization rules are enforced Partial (status codes) Yes (AC: non-admin blocked)
Business logic is correct No Yes (AC: role change is immediate)
UI feedback is correct No Yes (AC: confirmation shown)
Accessibility of the feature No Yes (AC: keyboard navigable)
Visual regression No Yes (AC: theme variants match)
Performance under load No Yes (AC: response within 200ms)

How Typed Specs Differ

Dimension API Spec / Contract Testing Typed Specifications
Scope API surface (endpoints, schemas) Feature behavior (any testable AC)
What it specifies Shape and structure Expected outcomes and behaviors
Test types covered Contract tests, schema validation Unit, E2E, visual, a11y, perf — any
Typo detection Runtime (schema mismatch) Compile-time (keyof T)
Rename safety Partial ($ref for schemas) Full (IDE refactor)
Completeness check Schema diff (shape only) Scanner checks all ACs
Build integration Yes (contract test failure) Yes (quality gate exit code)
Drift resistance High (for API shape) High (for feature behavior)

The Complementary Insight

API specs and typed specs are not competing. They operate at different layers and cover different concerns:

┌─────────────────────────────────────────────────────┐
│ Feature: User Roles                                  │
│                                                      │
│  AC: Admin can assign roles                          │
│    ├── Contract test (API shape: POST /roles → 204)  │
│    └── E2E test (behavior: role appears in UI)       │
│                                                      │
│  AC: Non-admin cannot assign roles                   │
│    ├── Contract test (API shape: POST /roles → 403)  │
│    └── E2E test (behavior: button is disabled)       │
│                                                      │
│  AC: Role change takes effect immediately            │
│    └── Integration test (no API spec coverage)       │
│                                                      │
│  AC: Role changes are audited                        │
│    └── Unit test (no API spec coverage)              │
│                                                      │
│  AC: User sees confirmation                          │
│    └── E2E test (no API spec coverage)               │
│                                                      │
│  @FeatureTest(UserRolesFeature)                      │
│  @Implements for each AC above                       │
└─────────────────────────────────────────────────────┘

The OpenAPI spec verifies the contract between client and server. Typed specs verify that the feature's acceptance criteria are tested — regardless of whether those tests are contract tests, E2E tests, unit tests, or accessibility audits.

The healthiest setup uses both:

  • OpenAPI for API boundaries between services
  • Typed specs for feature-level acceptance criteria that span multiple test types
  • Contract tests (Pact) for consumer-provider alignment in microservices

One feature's ACs might include a contract test AND an E2E test AND a visual regression test. Typed specs link all three to the feature. OpenAPI validates only the contract test. They're complementary lenses on the same system.

When API Specs Are Sufficient

If your product IS an API — no frontend, no UI, no user-facing behavior beyond the API surface — then OpenAPI/AsyncAPI + contract testing may be all you need. The API spec IS the feature spec. Every AC is expressible as a schema constraint or status code.

But the moment you have a UI, accessibility requirements, performance criteria, or behavioral logic that isn't captured by "this endpoint returns this shape" — you need something more. Typed specs fill that layer.


Previous: Part V: Directory Conventions and Wiki Matrices Next: Part VII: C# Roslyn Source Generators — the same philosophy, different mechanics.