Generating Client SDKs and Stubs Automatically
Manually generating client SDKs from an OpenAPI spec is error-prone and blocks releases. Running openapi-generator inside a CI/CD pipeline generates, validates and publishes SDKs automatically on every spec change, with Spectral linting, breaking change detection and contract tests acting as gates.
Table of Contents
- 1. Why OpenAPI generation belongs in CI
- 2. Pipeline design: the four stages at a glance
- 3. Stage 1: Spectral linting and spec validation
- 4. Stage 2: breaking change detection with openapi-diff
- 5. Stage 3: client SDK and server stub generation
- 6. Stage 4: contract tests with Schemathesis
- 7. A complete GitHub Actions pipeline
- 8. CI tools for OpenAPI compared
- 9. Summary
- 10. FAQ
1. Why OpenAPI generation belongs in CI
The most common weakness in OpenAPI-first projects is not spec quality, it is the process around spec changes. When the spec is maintained manually, SDKs are generated by hand and distribution to consumers happens over email or Slack, the result is stale clients, forgotten consumers and releases that wait on "someone regenerate the SDK". The fix: every change to the OpenAPI spec triggers an automated pipeline that validates the spec, refuses to let breaking changes slip through, generates fresh SDKs, runs contract tests and publishes the result to a package registry.
The benefits of this automation are layered. First, quality assurance. Linting and contract tests prevent faulty specs, or implementations that drift from the spec, from reaching production. Second, speed. SDK consumers get new versions automatically, with no manual effort on the API team's side. Third, transparency. Every spec change is a git commit with history, code review and an automatic breaking change report. The effort of setting up the initial pipeline pays for itself within a few weeks for any team with two or more SDK consumers.
One point that is often overlooked: the pipeline should not run only on the main branch. Spec changes in pull requests should already be checked for linting issues and breaking changes, so reviewers see the breaking change report right in the PR and can discuss it with full context. The gate for breaking changes should be optional on non-release branches (a warning rather than an error) but mandatory on the release branch.
2. Pipeline design: the four stages at a glance
A complete OpenAPI CI/CD pipeline consists of four sequential stages that build on each other: linting validates the spec for syntax errors and standards compliance, breaking change detection compares the spec against the previous version, code generation produces client SDKs and server stubs, and contract tests validate whether the running API implementation actually matches the spec. Each stage blocks the next one on failure, so early errors prevent expensive downstream work.
The spec itself should live in its own repository or directory with strict version control. That allows the spec and the implementation to be versioned separately, enables pull request reviews for spec changes, and creates a clear audit trail of every API design decision. If the spec and the code live in the same repository, a monorepo tool such as Turborepo or Nx should be used so that only the relevant pipelines trigger when the spec changes.
# .spectral.yaml: custom linting rules for API consistency
extends:
- spectral:oas # OpenAPI Spectral ruleset
rules:
# Every operation must have an operationId
operation-operationId:
severity: error
# operationId must be camelCase
operation-operationId-camel-case:
message: "operationId '{{value}}' must be camelCase"
given: "$.paths.*.*.operationId"
severity: warn
then:
function: pattern
functionOptions:
match: "^[a-z][a-zA-Z0-9]*$"
# All operations must have at least one tag
operation-tags:
severity: error
# All 4xx responses must be documented
response-4xx-required:
message: "Operation must document 400, 401, and 404 responses"
given: "$.paths.*.*"
severity: warn
then:
function: schema
functionOptions:
schema:
required: [responses]
properties:
responses:
required: ['400', '401']
# No inline schemas in operations, must use $ref
no-inline-schemas:
message: "Use $ref instead of inline schemas for reusability"
given: "$.paths.*.*.requestBody.content.*.schema"
severity: warn
then:
function: truthy
field: "$ref"
3. Stage 1: Spectral linting and spec validation
Spectral is the standard linter for OpenAPI specifications. It checks YAML/JSON files against a ruleset and reports errors and warnings with exact line numbers and descriptions. The default ruleset (spectral:oas) checks for JSON Schema validity, missing operationId fields, undocumented parameters and other common issues. Custom rules can be added to enforce project-specific conventions.
Spectral should run in two modes. In strict mode (--fail-severity=error) the pipeline fails on errors, warnings are only reported. In warning-report mode all warnings are collected into a file and attached to the CI job as an artifact, which allows tracking trends over time without breaking the build. When introducing Spectral into an existing project, it is best to start with a small set of strict rules and expand the ruleset gradually as existing issues get fixed.
In addition to Spectral, the redocly lint command should run as a second validation layer. Redocly checks for external references that cannot be resolved, circular references and other structural problems that Spectral does not catch. Together, the two tools reliably cover the most important classes of spec problems.
4. Stage 2: breaking change detection with openapi-diff
openapi-diff (or the alternatives oasdiff and Redocly openapi-cli diff) compares two versions of an OpenAPI specification and classifies changes as breaking, non-breaking or unclassified. Breaking changes in REST APIs include, among others: removing an endpoint or operation, renaming or removing fields in responses, adding required fields to requests without a default value, changing data types, and tightening validation rules (for example a lower maxLength value).
Breaking change detection compares the current spec against the version most recently tagged on the release branch. The result is inserted as a report in the PR comments, so reviewers immediately see which changes are breaking and whether the API version needs to be bumped. On the release branch, a breaking change blocks the deploy, unless the API version was manually bumped in the spec, which counts as an explicit acknowledgement that the breaking change is intentional.
#!/usr/bin/env bash
# scripts/check-breaking-changes.sh: compare current spec vs. last release tag
set -euo pipefail
CURRENT_SPEC="./openapi.yaml"
PREVIOUS_SPEC_TAG="${GITHUB_BASE_REF:-main}"
# Fetch previous spec from git tag
git show "origin/${PREVIOUS_SPEC_TAG}:openapi.yaml" > /tmp/previous-openapi.yaml 2>/dev/null || {
echo "No previous spec found, skipping breaking change check (first release)"
exit 0
}
# Run oasdiff (faster Go-based alternative to openapi-diff)
docker run --rm \
-v "$(pwd):/workspace" \
-v "/tmp:/tmp" \
tufin/oasdiff:latest \
breaking \
/tmp/previous-openapi.yaml \
/workspace/openapi.yaml \
--format=markdown \
> /tmp/breaking-changes.md
BREAKING_COUNT=$(grep -c "^##" /tmp/breaking-changes.md 2>/dev/null || echo "0")
if [[ $BREAKING_COUNT -gt 0 ]]; then
echo "::warning::${BREAKING_COUNT} breaking change(s) detected"
cat /tmp/breaking-changes.md
# On release branch: block. On feature branch: warn only.
if [[ "${GITHUB_REF_NAME:-}" == "main" ]]; then
echo "::error::Breaking changes not allowed on main without version bump"
exit 1
fi
fi
echo "Breaking change check completed, ${BREAKING_COUNT} breaking change(s) found"
5. Stage 3: client SDK and server stub generation
openapi-generator supports more than 50 generators for various languages and frameworks. The most relevant ones for a Symfony backend: php or php-nextgen for PHP clients, typescript-fetch or typescript-axios for frontend teams, python for data science teams, and php-symfony for server stubs. Every generator has extensive configuration options via the --additional-properties flag or an openapitools.json configuration file.
The quality of the generated code depends critically on the quality of the OpenAPI spec. Missing operationId fields produce automatically generated, cryptic function names. Missing schema descriptions produce uncommented properties. Missing examples produce empty test fixtures. This is another reason why linting comes before code generation: bad specs produce bad generated code that then has to be fixed by hand.
Generated SDKs should not be checked in directly, they should be published to a package registry: Packagist or a private Composer repository (Satis, Private Packagist) for PHP clients, npm or a private npm registry for TypeScript clients, PyPI for Python clients. The versioning scheme of the SDK package should follow the API version: a breaking change in the API bumps the SDK's major version, non-breaking changes bump the minor version.
# openapi-generator configuration: multiple SDKs from one spec
# openapitools.json
{
"$schema": "https://openapi-generator.tech/schemas/config.json",
"generators": {
"php-client": {
"generatorName": "php-nextgen",
"inputSpec": "./openapi.yaml",
"outputDir": "./generated/php-client",
"additionalProperties": {
"invokerPackage": "Mironsoft\\ApiClient",
"composerPackageName": "mironsoft/api-client",
"phpVersion": "8.4",
"useOneOfDiscriminatorLookup": true,
"composerProjectDescription": "Mironsoft Commerce API PHP Client"
}
},
"typescript-client": {
"generatorName": "typescript-fetch",
"inputSpec": "./openapi.yaml",
"outputDir": "./generated/typescript-client",
"additionalProperties": {
"npmName": "@mironsoft/api-client",
"npmVersion": "{{API_VERSION}}",
"supportsES6": true,
"withSeparateModelsAndApi": true,
"typescriptThreePlus": true
}
},
"php-server-stub": {
"generatorName": "php-symfony",
"inputSpec": "./openapi.yaml",
"outputDir": "./src/Generated",
"globalProperties": {
"modelTests": "false",
"apiTests": "false"
},
"additionalProperties": {
"invokerPackage": "App\\Generated",
"apiPackage": "App\\Generated\\Api",
"modelPackage": "App\\Generated\\Model"
}
}
}
}
# Generate all SDKs:
# docker run --rm -v $(pwd):/local openapitools/openapi-generator-cli:latest \
# batch /local/openapitools.json
6. Stage 4: contract tests with Schemathesis
Schemathesis is a property-based testing tool for REST APIs that automatically generates test cases from an OpenAPI specification and runs them against a live API instance. Unlike manually written tests, Schemathesis automatically generates edge cases and boundary values that human testers rarely think of: very long strings, Unicode special characters, negative numbers, empty arrays, null values and invalid formats. Many bugs in REST APIs, unhandled exceptions, wrong status codes, schema mismatches, get found by Schemathesis without a single test having been written by hand.
The simplest Schemathesis invocation: schemathesis run openapi.yaml --base-url http://localhost:8080 --checks all. That checks whether all responses match the documented status codes and schemas. For security-relevant checks: --checks=not_a_server_error ensures that no 5xx responses are produced. The stateful=all mode chains operations based on the links definitions in the spec and tests realistic workflows (create, read, update, delete). Schemathesis reports are stored as CI artifacts and contain exact reproduction commands for every bug found.
7. A complete GitHub Actions pipeline
The following pipeline implements all four stages in a single GitHub Actions workflow file. It runs on pull requests (linting plus breaking change warning) and on main pushes (the full pipeline including SDK publishing). The pipeline uses Docker for every OpenAPI tool, to avoid version drift and guarantee a reproducible environment.
# .github/workflows/openapi-pipeline.yml
name: OpenAPI CI Pipeline
on:
pull_request:
paths: ['openapi.yaml', 'openapi/**']
push:
branches: [main]
paths: ['openapi.yaml', 'openapi/**']
jobs:
lint:
name: Spec Linting
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Install Spectral
run: npm install -g @stoplight/spectral-cli
- name: Run Spectral
run: spectral lint openapi.yaml --fail-severity=error
- name: Run Redocly lint
uses: docker://redocly/cli:latest
with:
args: lint openapi.yaml
breaking-changes:
name: Breaking Change Detection
runs-on: ubuntu-latest
needs: lint
steps:
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Check breaking changes
run: bash scripts/check-breaking-changes.sh
- name: Upload report
if: always()
uses: actions/upload-artifact@v4
with:
name: breaking-changes-report
path: /tmp/breaking-changes.md
generate-sdks:
name: Generate SDKs
runs-on: ubuntu-latest
needs: [lint, breaking-changes]
if: github.ref == 'refs/heads/main'
steps:
- uses: actions/checkout@v4
- name: Generate PHP Client
run: |
docker run --rm \
-v $(pwd):/local \
openapitools/openapi-generator-cli:latest \
generate \
-i /local/openapi.yaml \
-g php-nextgen \
-o /local/generated/php-client \
--additional-properties=invokerPackage=Mironsoft\\ApiClient
- name: Generate TypeScript Client
run: |
docker run --rm \
-v $(pwd):/local \
openapitools/openapi-generator-cli:latest \
generate \
-i /local/openapi.yaml \
-g typescript-fetch \
-o /local/generated/typescript-client \
--additional-properties=npmName=@mironsoft/api-client
- name: Publish PHP package
working-directory: generated/php-client
run: |
# Update version from openapi.yaml info.version
API_VERSION=$(grep '^ version:' ../../openapi.yaml | awk '{print $2}' | tr -d '"')
sed -i "s/\"version\": \".*\"/\"version\": \"${API_VERSION}\"/" composer.json
# Publish to private Satis registry
curl -X POST "${{ secrets.SATIS_WEBHOOK_URL }}" \
-H "Authorization: Bearer ${{ secrets.SATIS_TOKEN }}"
8. CI tools for OpenAPI compared
The OpenAPI tooling landscape is large and moves fast. The following table compares the most important tools for the four pipeline stages, so teams can pick the tooling that suits their stack.
| Stage | Tool | Strength | Recommendation |
|---|---|---|---|
| Linting | Spectral + Redocly | Spectral: custom rules. Redocly: structural checks | Combine both |
| Breaking Changes | oasdiff (Go) / openapi-diff (Java) | oasdiff: fast, active. openapi-diff: mature | oasdiff for new projects |
| Code Generation | openapi-generator / kiota | openapi-generator: 50+ languages. kiota: MS stack, type-safe | openapi-generator for PHP/TS |
| Contract Tests | Schemathesis / Dredd | Schemathesis: property-based, automatic. Dredd: simpler | Schemathesis for deep tests |
| Mock Server | Prism / openapi-mock | Prism: valid responses from examples. openapi-mock: Docker-ready | Prism for local development |
9. Summary
A complete OpenAPI CI/CD stack made of four stages, Spectral linting, breaking change detection, openapi-generator SDK generation and Schemathesis contract tests, automates the entire cycle from spec change to a published SDK version. The key is sequential execution: bad specs never produce SDKs, breaking changes on release branches block deploys, and implementation deviations from the spec get caught before the merge.
The initial setup effort for this pipeline is one to two days, and for a team with several API consumers it pays for itself within a few releases. The long-term value is not just the automation, it is the governance: every API change becomes a reviewable, testable, documented process. No SDK stays stale. No breaking change slips through unnoticed. No frontend team waits on a backend developer who forgot to regenerate the SDK.
OpenAPI Generator in CI: The Essentials at a Glance
Stage 1: Linting
Spectral with custom rules plus Redocly lint. Checks syntax, conventions and structural issues. Blocking on errors, warning report as an artifact.
Stage 2: Breaking Changes
oasdiff compares the current spec against the last release tag. PR comment with the report. Blocking on the main branch for breaking changes without a version bump.
Stage 3: SDK Generation
openapi-generator via Docker. PHP client, TypeScript client, server stubs. Automatic publishing to a package registry with the API version as the SDK version.
Stage 4: Contract Tests
Schemathesis against the staging API. Property-based: automatic edge cases. stateful=all for workflow tests. Reports as CI artifacts with reproduction commands.
Mironsoft
CI/CD automation, OpenAPI pipelines and SDK generation
Want a complete OpenAPI CI/CD pipeline set up?
We build complete OpenAPI CI/CD pipelines, from Spectral linting and breaking change detection to automatic SDK generation and Schemathesis contract tests in GitHub Actions or GitLab CI.
Pipeline Setup
Setting up all four pipeline stages in GitHub Actions or GitLab CI
SDK Automation
Automatic SDK generation and publishing to package registries
Contract Testing
Schemathesis integration for automatic API implementation tests