Engineering

OpenAPI best practices for SDK-friendly specs

OpenAPI is portable enough that you can generate documentation, SDKs, and mocks from the same file. Whether the output of those generators is good depends on choices buried in the spec. This is the short list of the patterns that make a spec produce SDK code your customers will actually like.

1. Use operationId everywhere, and make it count

Every operation should have an operationId. Most SDK generators (including Bloom's) use it to name the corresponding method.

Good:

paths:
  /messages:
    post:
      operationId: sendMessage
      tags: [Messages]

Bad:

paths:
  /messages:
    post:
      tags: [Messages]
      # no operationId — generator falls back to "messagesPost" or similar

The naming convention you pick (sendMessage vs messages_send vs messagesSend) becomes the method name in TypeScript and Python. Pick a convention and apply it consistently — the generator can transform camelCasesnake_case, but it can't invent intent.

2. Group with tags, then let the generator nest

Bloom and most generators turn tags into resource namespaces in the generated SDK. tags: [Messages, Outbound] becomes client.messages.outbound.send(...).

Keep tags semantically meaningful (resource names), not organizationally meaningful (team names). Customer code reads client.messages.send, not client.platform_team_v2.send.

3. Reuse with components.schemas, not inline

Every schema that appears in more than one operation should be defined once under components.schemas: and referenced with $ref. Inline duplicates produce duplicate generated types with names like SendMessageResponse_1 and GetMessageResponse_2 that aren't interchangeable.

Good:

components:
  schemas:
    Message:
      type: object
      properties:
        id: { type: string }
        content: { type: string }
paths:
  /messages:
    post:
      responses:
        "200":
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Message'

Bad: the same shape inline in two operations. The SDK ends up with two unrelated types.

4. Use oneOf + discriminator for sum types

If a field can be one of several shapes, model it as a discriminated union, not as a wide object with optional fields.

components:
  schemas:
    Event:
      oneOf:
        - $ref: '#/components/schemas/MessageEvent'
        - $ref: '#/components/schemas/StatusEvent'
      discriminator:
        propertyName: type
        mapping:
          message: '#/components/schemas/MessageEvent'
          status: '#/components/schemas/StatusEvent'

A spec like this produces a real discriminated union in TypeScript (MessageEvent | StatusEvent) and idiomatic Union[MessageEvent, StatusEvent] in Python. Customer code uses narrowing instead of if event.message_field is not None.

5. Provide example: on every operation

For every request body, query parameter, and response, set an example (or examples for multiple). This is the single biggest quality lever for generated docs and SDK READMEs.

paths:
  /messages:
    post:
      requestBody:
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/SendMessageRequest'
            example:
              number: "+15551234567"
              from_number: "+15557654321"
              content: "Hello"

Without examples, generators fall back to schema-driven defaults that look like { "number": "string", "content": "string" }. With examples, the SDK README and docs pages have a runnable snippet from day one.

6. Put auth in components.securitySchemes and apply at the operation level

Don't repeat auth definitions across operations. Define each scheme once under components.securitySchemes: and reference it on the operations that require it.

If your API has one auth scheme that applies to all operations, define it at the top level under security: once. Use the operation-level override only for endpoints that are different (e.g. an unauthenticated /health or a different scheme for /webhooks).

7. Use nullable (OpenAPI 3.0) or type: ["string", "null"] (3.1) for genuine nulls

A field that can return null is a different shape from a field that can be missing. The generator emits string | null (TS) or Optional[str] (Python) for nullable, and string | undefined / Optional[str] for optional. Customers care about the difference.

In OpenAPI 3.1, the JSON Schema-aligned form is cleaner:

properties:
  phone:
    type: [string, "null"]

In 3.0:

properties:
  phone:
    type: string
    nullable: true

8. Mark security-sensitive fields explicitly

Fields like password, api_key, and client_secret should have format: password (or a custom x-secret: true you respect in your generators). Generators that understand this will scrub the field from logs and from generated docs examples.

9. Set servers: precisely

servers: is the source of the SDK's default baseURL. Set the production URL explicitly; use templating only when you have multiple environments your SDK is expected to switch between.

servers:
  - url: https://api.example.com
    description: Production
  - url: https://sandbox.example.com
    description: Sandbox

Bloom's generated TypeScript SDK uses the first servers[0].url as the default and lets customers override with new AcmeAPI({ baseURL: '...' }) or ACME_API_BASE_URL env var.

10. Use info.version for the spec version, not the API version

info.version is the spec's version (e.g. 2026.04.01 or 1.4.2). It is not your API's version (e.g. v2). The API version belongs in servers: (path-versioned) or as a required parameter (header-versioned). Don't conflate them.

What Bloom does with a clean spec

Bloom's SDK generator reads OpenAPI 3.0/3.1 and produces TypeScript and Python SDKs whose code quality is directly proportional to the spec's quality. A spec following the above patterns produces SDKs with:

  • Method names that read like English
  • One TypeScript interface per logical schema, with type narrowing through discriminated unions
  • README snippets with real examples
  • Idiomatic null vs optional handling per language

If you want to know how your spec scores on these, start a free Bloom report — the generation report flags schema duplication, missing examples, and operations without operationId.

Quick checklist

  • Every operation has an operationId
  • Tags are resource names, not team names
  • Every schema lives once under components.schemas
  • Sum types use oneOf + discriminator
  • Every operation has at least one example
  • Auth is in securitySchemes, applied at the right scope
  • Nullable fields are marked correctly
  • Sensitive fields are flagged
  • servers: has the production URL explicitly
  • info.version is the spec version, not the API version