OpenAPI Contract Validation - Quick Reference
When NOT to Use This Skill
- GraphQL APIs - Use
graphql-contractskill - Authentication flows - Use
auth-flow-validationskill - API versioning strategy - Use
api-versioningskill - Type generation setup - Use
type-generationskill
Deep Knowledge: Use mcp__api-explorer__get_api_endpoint_details for specific endpoint validation.Contract Validation Overview
┌─────────────────────────────────────────────────────────────┐
│ CONTRACT VALIDATION │
├─────────────────────────────────────────────────────────────┤
│ │
│ Frontend Code OpenAPI Spec Backend │
│ ┌──────────────┐ ┌────────────┐ ┌──────────────┐ │
│ │ fetch('/api/ │ ←──→ │ paths: │ ←──→│ @Controller │ │
│ │ users') │ │ /users: │ │ @GetMapping │ │
│ │ │ │ get: │ │ │ │
│ │ type User { │ ←──→ │ schemas: │ ←──→│ class UserDto│ │
│ │ id: number │ │ User: │ │ Long id │ │
│ │ } │ │ id:int │ │ │ │
│ └──────────────┘ └────────────┘ └──────────────┘ │
│ │
│ MUST MATCH SOURCE OF MUST MATCH │
│ TRUTH │
└─────────────────────────────────────────────────────────────┘Common Discrepancy Types
1. Path Mismatch
# OpenAPI Spec
paths:
/users/{userId}: # Backend path
get: ...
# Frontend Code
fetch('/api/users/${id}') # Frontend uses different path!Detection:
# Find API calls in frontend
grep -r "fetch\|axios\|http\." src/
# Compare with OpenAPI paths
# Use api-explorer to list pathsFix: Update frontend to use correct path or configure base URL.
2. Type Mismatch
# OpenAPI Spec
components:
schemas:
User:
properties:
age:
type: string # Backend uses string
# Frontend Code
interface User {
age: number; # Frontend expects number!
}Detection:
// Frontend type
interface CreateUserDto {
age: number; // MISMATCH
}
// Should be
interface CreateUserDto {
age: string; // Match OpenAPI spec
}3. Required Field Missing
# OpenAPI Spec
components:
schemas:
CreateUserRequest:
required:
- email
- name
- role # Required!
properties:
email: { type: string }
name: { type: string }
role: { type: string }
# Frontend Code
const payload = {
email: user.email,
name: user.name,
// role is missing! <-- Will fail validation
};4. Response Structure Mismatch
# OpenAPI Spec - Paginated response
paths:
/users:
get:
responses:
200:
content:
application/json:
schema:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/User'
meta:
$ref: '#/components/schemas/PaginationMeta'
# Frontend Code - Expects array directly
const users: User[] = await response.json(); // WRONG!
// Should be
const { data, meta } = await response.json();
const users: User[] = data;Validation Checklist
Per-Endpoint Validation
| Check | Frontend | Backend (OpenAPI) | Status |
|---|---|---|---|
| Path | /api/users | /users | MISMATCH |
| Method | POST | POST | OK |
| Content-Type | application/json | application/json | OK |
| Request Body | matches schema | CreateUserRequest | CHECK |
| Response Type | matches schema | User | CHECK |
| Status Codes | handles 400, 401 | 200, 400, 401, 500 | PARTIAL |
Request Body Validation
// OpenAPI Schema
interface CreateUserRequest {
email: string; // required
name: string; // required
age?: number; // optional
role: UserRole; // required, enum
}
// Validate frontend matches
interface FrontendPayload {
email: string; // OK - required
name: string; // OK - required
age?: number; // OK - optional
role: 'admin' | 'user'; // CHECK - enum values match?
}Response Handling Validation
// OpenAPI Response
interface ApiResponse<T> {
data: T;
meta?: {
page: number;
total: number;
};
error?: {
code: string;
message: string;
};
}
// Frontend should handle all cases
async function fetchUsers(): Promise<User[]> {
const response = await fetch('/api/users');
if (!response.ok) {
const error = await response.json();
throw new ApiError(error.error.code, error.error.message);
}
const result: ApiResponse<User[]> = await response.json();
return result.data;
}Contract-First Development
1. Define OpenAPI Spec First
# openapi.yaml
openapi: 3.0.3
info:
title: User API
version: 1.0.0
paths:
/users:
post:
operationId: createUser
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/CreateUserRequest'
responses:
'201':
description: User created
content:
application/json:
schema:
$ref: '#/components/schemas/User'
components:
schemas:
CreateUserRequest:
type: object
required: [email, name]
properties:
email:
type: string
format: email
name:
type: string
minLength: 2
maxLength: 100
User:
type: object
properties:
id:
type: string
format: uuid
email:
type: string
name:
type: string
createdAt:
type: string
format: date-time2. Generate Types for Both Ends
# Frontend (TypeScript)
npx openapi-typescript openapi.yaml -o src/api/types.ts
# Backend (Java/Spring)
npx @openapitools/openapi-generator-cli generate \
-i openapi.yaml \
-g spring \
-o generated/3. Implement Against Generated Types
// Frontend - uses generated types
import type { CreateUserRequest, User } from './api/types';
async function createUser(data: CreateUserRequest): Promise<User> {
const response = await fetch('/api/users', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data),
});
return response.json();
}// Backend - uses generated DTOs
@PostMapping("/users")
public ResponseEntity<User> createUser(
@Valid @RequestBody CreateUserRequest request) {
// Implementation uses generated types
}MCP api-explorer Usage
Efficient Validation Queries
// 1. Search for endpoint
await mcp__api_explorer__search_api({
query: "users",
searchIn: ["paths"],
limit: 10
});
// 2. Get specific endpoint details
await mcp__api_explorer__get_api_endpoint_details({
path: "/users",
method: "POST"
});
// 3. Get request/response schemas
await mcp__api_explorer__get_api_models({
model: "CreateUserRequest",
compact: true
});Avoid Token Waste
// DON'T - Loads entire spec
await mcp__api_explorer__get_api_schema({ format: "full" });
// DO - Query specific endpoints
await mcp__api_explorer__get_api_endpoint_details({
path: "/users/{id}",
method: "GET"
});Automated Contract Testing
Prism (Mock Server)
# Start mock server from OpenAPI spec
npx @stoplight/prism-cli mock openapi.yaml
# Run frontend tests against mock
npm test -- --api-url=http://localhost:4010Schemathesis (API Fuzzing)
# Test backend against OpenAPI spec
schemathesis run openapi.yaml --base-url=http://localhost:8080
# Validate all endpoints
schemathesis run openapi.yaml \
--checks all \
--validate-schemaDredd (Contract Testing)
# Test implementation matches spec
dredd openapi.yaml http://localhost:8080CI/CD Integration
GitHub Actions
name: Contract Validation
on: [push, pull_request]
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Validate OpenAPI spec
run: npx @redocly/cli lint openapi.yaml
- name: Generate types
run: npx openapi-typescript openapi.yaml -o src/api/types.ts
- name: Check types unchanged
run: git diff --exit-code src/api/types.ts
- name: Run contract tests
run: npm run test:contractAnti-Patterns
| Anti-Pattern | Why It's Bad | Correct Approach |
|---|---|---|
| Manual type sync | Drift over time | Generate from spec |
| Ignoring OpenAPI spec | No source of truth | Contract-first development |
| Hardcoded URLs | Environment issues | Configure base URL |
| No validation in CI | Breaks discovered late | Automated contract tests |
| Any types for API | No type safety | Generate proper types |
Quick Troubleshooting
| Issue | Likely Cause | Solution |
|---|---|---|
| Types out of sync | Manual updates | Regenerate from spec |
| 404 errors | Path mismatch | Check OpenAPI paths |
| 400 Bad Request | Missing required field | Validate against schema |
| Unexpected response | Response structure changed | Update frontend types |
| CORS errors | Backend config | Check allowed origins |