OpenAPI Codegen: Generating Clients and Server Stubs Without Cruft
AI generated
{ }
GET
OpenAPI · Codegen · openapi-generator · CI/CD
OpenAPI Codegen: Generating Clients and Server Stubs Without Cruft
clean generated code without manual rework

Generating code from OpenAPI specifications sounds tempting, but in practice it often produces unreadable cruft that immediately needs manual patching. That defeats the whole point of generation. This article shows how to configure openapi-generator so the output code is maintainable, and why template overrides are the key to that.

16 min read openapi-generator · template overrides · CI integration · versioning PHP · TypeScript · Go · OpenAPI 3.1

1. Why codegen fails, and why it still pays off

The typical failure mode of codegen projects follows a predictable pattern: a team generates an API client from the OpenAPI specification, finds that the generated code is unusable in certain areas, and starts manually adjusting it. On the next generation run, those manual changes get overwritten. The team stops regenerating and maintains the client manually from then on. Six months later the client is out of date and the team has the same maintenance burden as before, plus a hard-to-read code style inherited from the original generation as a legacy.

The mistake is not codegen as a concept, but the assumption that a generator delivers production-ready code out of the box with zero configuration. Like any tool, openapi-generator needs an initial investment: choosing a generator, a configuration file, template overrides for project-specific conventions, and a clear strategy for what gets generated versus what gets hand-written. That investment pays off: when the API specification changes, the client is updated in seconds, typos in endpoint URLs and field names become impossible, and new developers do not need to read manual documentation to understand the client.

2. openapi-generator: choosing a generator and basic setup

The OpenAPITools/openapi-generator project supports more than 60 target languages and frameworks. Picking the right generator for the target language is not trivial: for PHP there is php (Guzzle-based), php-nextgen (PSR-18, more modern), and for Symfony projects php-symfony directly for server stubs. For TypeScript frontends there is typescript-fetch, typescript-axios, and typescript-node. Each generator has its own options, template structures, and quality levels, so a careful comparison of the output pays off before deciding.

The recommended installation method for teams is the Docker image, which encapsulates all Java dependencies. A shell wrapper script in the project root ensures that every developer uses the same generator version. The alternative, npx @openapitools/openapi-generator-cli, is more convenient for Node projects, but the generator version must be pinned explicitly, because different versions have different template defaults, and an unnoticed version bump can change the generated artifacts unexpectedly.


#!/usr/bin/env bash
# bin/generate-client - wrapper for consistent generator version
set -euo pipefail

GENERATOR_VERSION="7.5.0"
SPEC_FILE="${1:-openapi.json}"
OUTPUT_DIR="${2:-generated/php-client}"
CONFIG_FILE="${3:-codegen/php-client.yaml}"

echo "Generating PHP client from ${SPEC_FILE}..."

docker run --rm \
  -v "$(pwd):/workspace" \
  -w /workspace \
  "openapitools/openapi-generator-cli:v${GENERATOR_VERSION}" generate \
    --input-spec "/workspace/${SPEC_FILE}" \
    --generator-name php-nextgen \
    --output "/workspace/${OUTPUT_DIR}" \
    --config "/workspace/${CONFIG_FILE}" \
    --skip-validate-spec

echo "Generation complete: ${OUTPUT_DIR}"

Use the --skip-validate-spec flag with care: it skips schema validation and allows generation from a slightly invalid specification. In a mature project, where the specification runs against a linter, this flag should not be set. During the bootstrapping phase of a migration it is useful to allow generation despite still-open schema errors.

3. Configuration file: every lever in one place

The configuration file (YAML or JSON) is the centerpiece of a maintainable codegen strategy. It defines which namespace is used, how classes and files are named, which features are enabled, and which template overrides are loaded. All project-specific adjustments belong in this file, not in shell arguments that drift across different scripts. Versioned in the repository, it represents the complete specification of the generation process.

The most important configuration options for PHP projects are invokerPackage for the namespace, packageName for the Composer package name, modelNamePrefix and modelNameSuffix for consistent class names, and apiNameSuffix. With withInterfaces: true the php-nextgen generator also generates an interface for every API class, which is useful for dependency injection in Symfony. With composerVendorName and composerProjectName, a complete composer.json for the generated package is produced.


# codegen/php-client.yaml - generator configuration
generatorName: php-nextgen
inputSpec: openapi.json
outputDir: generated/php-client

# PHP namespace and package settings
additionalProperties:
  invokerPackage: "Mironsoft\\ApiClient"
  apiPackage: "Mironsoft\\ApiClient\\Api"
  modelPackage: "Mironsoft\\ApiClient\\Model"
  composerVendorName: "mironsoft"
  composerProjectName: "api-client"
  phpVersion: "8.4"
  withInterfaces: true
  variableNamingConvention: camelCase
  packageName: "mironsoft-api-client"

# Template overrides directory
templateDir: codegen/templates/php-nextgen

# Global properties
globalProperties:
  generateAliasAsModel: true
  skipFormModel: true

# Files to skip (use .openapi-generator-ignore for per-file control)

4. Template overrides: replacing cruft with your own Mustache templates

Template overrides are the most important mechanism for adapting generated code to project conventions without patching the generator itself. Every openapi-generator generator uses Mustache templates that can be overridden by placing them in a local directory and pointing to it with --template-dir. Only the templates you place there are overridden; everything else keeps using the generator defaults.

Typical candidates for template overrides: the model class, so it uses readonly properties and constructor property promotion in PHP 8.4 instead of generated getter/setter boilerplate. The API class, so it uses PSR-18-compatible HTTP clients instead of embedded Guzzle configuration. The file header, so license headers and PHPDoc blocks are standardized. You export the original templates with openapi-generator author template -g php-nextgen -o templates/, copy the files you want to change into the local override directory, and adjust them.

5. Generating a PHP client: Guzzle, PSR-18, and namespace control

For PHP API clients, the choice between the classic php generator (tightly bound to Guzzle) and the more modern php-nextgen generator (PSR-18-compatible) is a strategic decision. PSR-18 lets you swap the HTTP client for any PSR-18-compatible adapter: Guzzle, Symfony HttpClient, cURL. In Symfony projects, that means the Symfony HttpClient can be injected directly as a PSR-18 client into the generated client without an adapter. This reduces dependencies and enables central configuration of timeouts, retries, and proxy settings.

A common problem with generated PHP clients is naming: fields transmitted by the API as created_at (snake_case) should be available in PHP code as createdAt (camelCase). The generator must be explicitly configured to apply this convention, and the serialization layer must be configured accordingly. With variableNamingConvention: camelCase in the configuration and a matching serializer (Symfony Serializer or JMS Serializer), this is solvable without template overrides.

6. TypeScript client: fetch-based without class overhead

For frontend teams, the typescript-fetch generator is often the better choice over typescript-axios, because it introduces no additional dependency and works with the native fetch API, which is available in all modern browsers and in Node.js. The result is plain functions instead of complex class hierarchies, which work better with tree-shaking in bundlers. With the option supportsES6: true and useSingleRequestParameter: true, the generator produces more compact, more modern API functions.

The generated TypeScript client contains fully typed request and response interfaces, derived directly from the OpenAPI schemas. When the API adds a new field, a regeneration updates the interface, and TypeScript immediately flags every place in the frontend code that ignores the new field or should use it. That is the decisive advantage over hand-written clients: breaking changes in the API become compile errors in client code before they reach production.


# codegen/typescript-client.yaml
generatorName: typescript-fetch
inputSpec: openapi.json
outputDir: generated/typescript-client

additionalProperties:
  supportsES6: true
  useSingleRequestParameter: true
  withInterfaces: true
  npmName: "@mironsoft/api-client"
  npmVersion: "1.0.0"
  typescriptThreePlus: true
  modelPropertyNaming: camelCase
  enumPropertyNaming: UPPERCASE

templateDir: codegen/templates/typescript-fetch

# Generated file: generated/typescript-client/src/apis/ProductsApi.ts
# Usage in frontend:
#
# import { ProductsApi, Configuration } from '@mironsoft/api-client';
#
# const api = new ProductsApi(new Configuration({
#   basePath: process.env.NEXT_PUBLIC_API_URL,
#   accessToken: () => getAuthToken(),
# }));
#
# const product = await api.createProduct({
#   productCreateRequest: { name: 'Headphones', price: 4999, categoryId: uuid }
# });

7. Server stubs: generate interfaces, write the implementation yourself

Server stubs are the underrated use case for codegen. Instead of generating the entire server, you generate only the interfaces and abstract classes that define the API contract, and implement the business logic yourself. In Symfony, the php-symfony generator is well suited for this, producing controller interfaces and response DTOs. Every interface method corresponds to an API endpoint, with correctly typed parameters and return types. Symfony controllers implement these interfaces, and when the API specification changes and the interface gets a new method, PHP forces a compile error until every controller implementation is updated.

This approach is especially valuable for versioned APIs. When a new API version introduces a different schema, you generate the new interfaces and implement them separately, while the old controllers keep implementing the v1 interfaces. The specification is the single source of truth for the contract; code that does not match the contract simply does not compile. That is strict contract enforcement without any additional runtime checking.

8. Should generated artifacts be versioned?

Whether generated artifacts should live in version control is one of the most common debates in codegen projects. The arguments against versioning: generated code is derived and does not belong in version control; only the source (the OpenAPI specification) is versioned, and the client is generated on demand. The arguments for versioning: developers can use the client immediately without installing the generator; diffs in review show exactly which API changes caused which code changes; the client is available even without the generation toolchain.

The pragmatic recommendation for most teams: version the generated client as a standalone package and publish it through the internal package registry (Composer Private Packagist, npm Registry). CI regenerates the client on every spec change, creates a new package version, and publishes it. Consumers of the client update the package version in their composer.json or package.json. This decouples client consumers from the generation toolchain and makes updates explicit and controllable.

9. Generator variants compared directly

Choosing the right generator for every combination of target language and requirement is decisive for the quality of the generated code. The following table gives an overview of the most important options.

Generator Target Language HTTP Client Recommendation
php-nextgen PHP 8.x PSR-18 (swappable) Recommended for PHP clients
php-symfony PHP / Symfony Controller interfaces Recommended for server stubs
typescript-fetch TypeScript native fetch Recommended for frontends
typescript-axios TypeScript axios When axios is already in use
go Go net/http For Go microservices

Regardless of the chosen generator: a template override for the file header that includes an @generated notice and a @do-not-edit comment prevents developers from accidentally editing generated files by hand. A pre-commit hook that checks whether generated files have been manually modified provides an additional safeguard.

Mironsoft

REST API design, code generation, and CI/CD integration

Want a clean generated API client without manual patches?

We set up openapi-generator with project-specific template overrides, configuration files, and CI pipelines for PHP and TypeScript projects, so every spec change automatically produces clean, maintainable client code.

Generator setup

Configuration file, template overrides, and wrapper script for consistent versions

CI integration

Automatic regeneration on spec changes, package publishing to a private registry

Server stubs

Generate Symfony controller interfaces from the OpenAPI spec and wire them into your existing architecture

10. Summary

Clean generated code requires upfront configuration work, but it pays off with every API update afterward. The decisive components: a versioned configuration file with all namespace, naming, and feature options. Template overrides for project-specific conventions such as PHP 8.4 syntax or TypeScript modularization. A wrapper script that pins the generator version, so all developers and CI produce identical output. A clear strategy for generated artifacts, where package publishing through a private registry decouples consumers from the generation toolchain.

The most important principle: never manually edit generated files. If the generated code is unsatisfactory somewhere, the fix is a template override or a configuration change, not a manual correction that gets lost on the next regeneration run. Teams that hold this line end up, after the initial setup effort, with a client that stays current with the specification with a single command.

OpenAPI Codegen Without Cruft: the essentials at a glance

Configuration file

All options in one versioned YAML file: namespace, naming, features. No loose shell arguments.

Template overrides

Override Mustache templates for project-specific conventions: PHP 8.4 syntax, TypeScript modules, file headers.

Pin the version

Docker image with a fixed generator version in the wrapper script: identical output on every developer machine and in CI.

No manual patching

Never manually edit generated files. Always solve improvements through template overrides or configuration.

11. FAQ: OpenAPI Codegen for Clients and Server Stubs

1Why do codegen projects fail so often?
Because teams manually patch the generated code instead of using template overrides. On the next generation run the changes get lost, and the team stops regenerating.
2php vs. php-nextgen?
php: fixed Guzzle binding. php-nextgen: PSR-18-compatible, HTTP client swappable. In Symfony, use the Symfony HttpClient directly as a PSR-18 adapter.
3Prevent manual edits to generated files?
@generated comment in the header template, a pre-commit hook that rejects changes to generated files, and .openapi-generator-ignore for selective generation.
4Version generated artifacts in Git?
Better to publish as a standalone package through a private registry. CI regenerates and publishes new versions. Consumers update explicitly.
5What are template overrides?
Export the Mustache templates from the generator, copy the files you want to change into a local directory, adjust them, and point to it with --template-dir. Only overridden templates deviate from the default.
6typescript-fetch instead of typescript-axios?
No additional dependency, tree-shakeable, works in browsers and Node.js. axios makes sense when it is already present in the project.
7What are server stubs?
Generated interfaces and abstract classes for the API contract. Controllers implement these interfaces; spec changes force compile errors until every implementation is updated.
8Pin the generator version for all developers?
A wrapper script with a Docker image and a fixed tag version: openapitools/openapi-generator-cli:v7.5.0, giving identical output on every machine and in CI.
9Generate only certain parts of the spec?
.openapi-generator-ignore file and globalProperties such as apis=false or models=false. Generating only model classes or only API classes is straightforward.
10Handle breaking changes in the API spec?
Breaking changes in the spec produce breaking changes in the generated code: TypeScript compile errors, PHP type errors. That makes breaking changes explicit before production.