Why Cloudflare rebuilt its SDK pipeline around OpenAPI

Cloudflare's SDKs for TypeScript, Go, and Python have been available since Developer Week 2024, but the path to releasing them was not straightforward. For years, each SDK was maintained manually, requiring at least four coordinated pull requests per change — one for the API schema, one per language SDK, and one for the Terraform provider. This manual process meant updates were uneven across languages, with the Go SDK receiving the most attention because it underpinned the Terraform provider. Even then, keeping that single SDK current was taxing, and there were no guarantees around coverage, parity, or correctness.

The turning point came after Cloudflare migrated its API definitions from JSON Hyper-Schema to OpenAPI. With a single source of truth in place, the team set a new goal: use OpenAPI not just for documentation, but to generate the SDKs themselves. Before committing to a generator, they defined guiding principles that would shape the pipeline and the SDKs it produces.

SDKs should feel native to their language

Generated SDKs often betray their origins — the code carries the flavor of the generator's implementation language rather than the target language's idiomatic patterns. For Cloudflare, that was unacceptable. A developer using Ruby, for example, would naturally write a single-line if expression, but a generic generator has no awareness of such conventions and would emit a verbose multi-line if/else block. The problem intensifies when generating a dynamically typed language from a strongly typed one: the output ends up structured around types that are never used. Users should not have to adapt their coding style to work around the SDK's provenance.

Uniform support means uniform naming

When a new API is added but never reaches the SDK a developer relies on, it might as well not exist. Cloudflare wanted new features to reach all supported languages at roughly the same time, with consistent namespaces and method patterns. If a developer knows how to list DNS records in one SDK, the same dns namespace and list method should exist in the others. That consistency reduces time spent hunting through documentation and lets developers work across SDKs with minimal friction.

Feedback loops start in CI

Cloudflare operates a large number of APIs, and not all of them have been designed with equal rigor. Endpoints that see heavy traffic or frequent malformed input tend to accumulate better hardening than rarely used ones. To close that gap, the OpenAPI pipeline includes linting rules enforced via redocly CLI, which can warn or block a change depending on severity.

One enforced convention is presenting fine-grained API token authentication before other schemes. A redocly plugin can be written to check the order and presence of authentication schemes in the schema, and a corresponding rule configuration makes the CI run fail if the convention is violated. The failure message points the engineer to documentation explaining the rationale. Similar lints enforce style rules for documentation descriptions, such as starting with a capital letter and ending with a period.

This approach lets teams ship endpoints that meet established quality bars without requiring deep knowledge of every design pattern Cloudflare has accumulated over the years.

Choosing a generator instead of building one

Early analysis suggested that building an in-house SDK generator would take at least six to nine months to produce a single high-quality SDK, with more work required for each additional language. That timeline was incompatible with the goal of adding languages cheaply later. Evaluation of off-the-shelf options initially came up short — most could not handle the size and complexity of Cloudflare's schemas. Then the team tried Stainless, a platform founded by an engineer who previously built API tooling at Stripe. The OpenAI Python and TypeScript SDKs, among others, are generated by Stainless.

With Stainless, developers bring their OpenAPI schemas and map them to SDK methods using a configuration file. That configuration is what allows operations like client.zones.list() to be generated consistently across languages. The arrangement keeps most changes within the existing API schemas, while SDK-specific behavior adjustments can be handled per language through the configuration file.

Using an external generator also clarified responsibility:

  • Service teams own the representation of their product for end users.
  • The API team centralizes tooling and conventions, and translates service mappings into Stainless configuration.
  • Stainless handles consistent SDK generation across languages.

The result is a workflow where the majority of changes ship through a single pull request, even if a new language or integration is added to the pipeline. In a few months, Cloudflare went from inconsistently maintained, manually updated SDKs to automatically generated libraries in three languages with updates flowing directly from internal teams.

Scale refactors with codemods

Cloudflare's public API surface has grown to roughly 1,300 endpoints, each with its own historical quirks—inconsistent path parameters, varying HTTP methods, and divergent naming conventions. Handling these differences individually doesn't scale, so the team turned to codemods: programmatic transformations that rewrite code with awareness of the underlying language structure, effectively a context-aware find-and-replace for large-scale refactoring.

Their first tool was comby, wrapped in a custom CLI that communicated with version control endpoints and generated pull request descriptions, commit messages, and a TOML configuration file for each transformation. A sample configuration shows how URI paths were normalized so plural resources followed by an individual identifier consistently use an _id suffix rather than _identifier, Identifier, or other variations.

[account-id-1-path-consistency]
match = 'paths/~accounts~1{account_identifier1}'
rewrite = 'paths/~accounts~1{account_id}'

[account-id-camelcase-path-consistency]
match = 'paths/~accounts~1{accountId}'
rewrite = 'paths/~accounts~1{account_id}'

[placeholder-identifier-to-id]
match = ':[_~_identifier}]' # need the empty hole match here since we are using unbalanced }
rewrite = '_id}'

[route-consistency-for-resource-plurals]
match = ':[topic~/\w+/]{:[id~\w+]}'
rewrite = ':[topic]{:[id]}'
rule = 'where rewrite :[id] { :[x] -> :[topic] }, rewrite :[id] { /:[x]s/ -> :[x]_id }'

[property-identifier-to-id]
match = 'name: :[topic]_identifier'
rewrite = 'name: :[topic]_id'

This worked for most internal changes, but the team wanted a migration tool customers could use themselves. comby proved popular for upgrades in the Terraform Provider, but its syntax gets difficult to read with complex expressions. They settled on Grit, which uses GritQL as a query language familiar to anyone who knows basic JavaScript. Grit also allowed Cloudflare to contribute migrations to the Grit Pattern Library, making them available as single CLI invocations.

// Migrate to the Golang v2 library 
grit apply cloudflare_go_v2

Schema hygiene pays off downstream

OpenAPI schemas must be consistent before feeding them into any generation pipeline, especially a homegrown one. Uniform structures make it trivial to distinguish bugs in the generator from bugs in individual schemas: if something fails everywhere, it's the pipeline; if only in one endpoint, it's the schema.

Consistency also drives developer experience. When routes always follow the same convention—plural resource, then identifier—users can infer inputs without hunting through documentation. Predictable conventions effectively compensate for gaps in docs.

Use $ref with restraint

Shared $ref values look appealing for reusability, but overuse creates schemas where finding correct values leads to cargo cult behaviors and hard-to-maintain code. Consider this example:

thing_base:
  type: object
  required:
    - id
  properties:
    updated_at:
      $ref: '#/components/schemas/thing_updated_at'
    created_at:
      $ref: '#/components/schemas/thing_updated_at'
    id:
      $ref: '#/components/schemas/thing_identifier'
      
thing_updated_at:
  type: string
  format: date-time
  description: When the resource was last updated.
  example: "2014-01-01T05:20:00Z"
  
thing_created_at:
  type: string
  format: date-time
  description: When the resource was created.
  example: "2014-01-01T05:20:00Z"

thing_id:
  type: string
  description: Unique identifier of the resource.
  example: "2014-01-01T05:20:00Z"

The created_at field carries the updated_at description—a bug easy to miss on first glance. Here it's just incorrect documentation; in other contexts it could produce a wholly wrong schema representation. Cloudflare recommends $ref mainly for component schemas intended for oneOf, allOf, or anyOf directives.

dns_record:
  oneOf:
    - $ref: '#/components/schemas/dns-records_ARecord'
    - $ref: '#/components/schemas/dns-records_AAAARecord'
    - $ref: '#/components/schemas/dns-records_CAARecord'
    - $ref: '#/components/schemas/dns-records_CERTRecord'
    - $ref: '#/components/schemas/dns-records_CNAMERecord'
    - $ref: '#/components/schemas/dns-records_DNSKEYRecord'
    - $ref: '#/components/schemas/dns-records_DSRecord'
    - $ref: '#/components/schemas/dns-records_HTTPSRecord'
    - $ref: '#/components/schemas/dns-records_LOCRecord'
    - $ref: '#/components/schemas/dns-records_MXRecord'
    - $ref: '#/components/schemas/dns-records_NAPTRRecord'
    - $ref: '#/components/schemas/dns-records_NSRecord'
    - $ref: '#/components/schemas/dns-records_PTRRecord'
    - $ref: '#/components/schemas/dns-records_SMIMEARecord'
    - $ref: '#/components/schemas/dns-records_SRVRecord'
    - $ref: '#/components/schemas/dns-records_SSHFPRecord'
    - $ref: '#/components/schemas/dns-records_SVCBRecord'
    - $ref: '#/components/schemas/dns-records_TLSARecord'
    - $ref: '#/components/schemas/dns-records_TXTRecord'
    - $ref: '#/components/schemas/dns-records_URIRecord'
  type: object
  required:
    - id
    - type
    - name
    - content
    - proxiable
    - created_on
    - modified_on

When unsure, apply the YAGNI principle. Extract shared schemas later, once enough real uses emerge to define the correct abstraction.

Design for usage first

Before writing generation code, the team authored language design documents for each target language using README-driven development. Focusing on library usability first surfaced problems early—Python keyword arguments, Go interfaces, enforcement of required parameters, client instantiation and overrides, and type handling were all settled on paper before implementation. That upfront work minimized unknowns throughout feature development.

Extending the approach

The OpenAPI pipeline was always meant to be a foundation, not a destination. With SDKs shipping for Go, TypeScript, and Python, Cloudflare now plans to generate the Terraform Provider with the same principles, further cutting maintenance overhead. Additional integrations with the Cloudflare Developer Platform are slated for later in 2024. Users seeking a different language can submit their preferences to help determine future SDK priorities.