Customizing SDK Method Names with Speakeasy Overlays
This guide is part of Speakeasy within SDK Generation & Changelog Automation. It covers shaping the public surface of a generated client — method names, namespaces, and parameter naming — using an OpenAPI Overlay rather than editing the specification. The need appears as soon as the first SDK ships: the generated names are derived from operationId and read like machine output, and the specification is either owned by another team or too widely shared to edit for one consumer’s benefit.
Problem & Context
A generator derives method names from the specification, and the specification was written for a different audience. operationId: getInvoiceByIdV2 is a perfectly good identifier for a contract and a poor method name for a library, so consumers end up writing client.getInvoiceByIdV2("inv_1") instead of client.invoices.get("inv_1").
The obvious fix — rename the operationId — has two problems. It changes the contract, which means it changes documentation URLs and anchors that other people have linked to. And the specification frequently is not yours to change: it may be generated from server annotations, owned by a platform team, or shared across services with its own review process.
The second-order problem is worse. Without namespaces, a large API produces a client with two hundred methods on one object. Autocomplete stops helping at that size, and consumers cannot discover related operations because nothing groups them.
An overlay solves both. It is a separate document describing modifications to apply to a target specification, so the naming decisions live in one small reviewable file, the contract stays untouched, and every language generator sees the same instructions.
Step-by-Step Solution
1. Inspect the names the generator currently emits
Measure before changing, so the improvement is visible and the diff is reviewable:
npm install --save-dev @speakeasy-api/speakeasy-client-sdk-typescript
speakeasy generate sdk --lang typescript --schema openapi/api.yaml --out /tmp/sdk-before
grep -roP '(?<=async )\w+(?=\()' /tmp/sdk-before/src/sdk/*.ts | sort | head
createInvoiceV2
deleteInvoiceByIdV2
getInvoiceByIdV2
listInvoicesV2
updateInvoiceByIdV2
2. Write an overlay instead of editing the spec
An overlay is a standard OpenAPI Overlay document: a list of JSONPath targets and the modifications to apply:
# overlays/sdk-naming.yaml
overlay: 1.0.0
info:
title: SDK naming for the Acme client libraries
version: 1.0.0
actions:
- target: $.paths["/invoices"].get
description: List becomes invoices.list
update:
x-speakeasy-group: invoices
x-speakeasy-name-override: list
- target: $.paths["/invoices"].post
update:
x-speakeasy-group: invoices
x-speakeasy-name-override: create
- target: $.paths["/invoices/{invoiceId}"].get
update:
x-speakeasy-group: invoices
x-speakeasy-name-override: get
- target: $.paths["/invoices/{invoiceId}"].patch
update:
x-speakeasy-group: invoices
x-speakeasy-name-override: update
- target: $.paths["/invoices/{invoiceId}"].delete
update:
x-speakeasy-group: invoices
x-speakeasy-name-override: delete
3. Group operations into namespaces
Groups compose, which is how a large surface becomes navigable rather than flat:
- target: $.paths["/admin/customers"].get
description: Nest under admin.customers
update:
x-speakeasy-group: admin.customers
x-speakeasy-name-override: list
// before
await client.listCustomersAdminV2();
// after
await client.admin.customers.list();
Renaming a parameter is the same mechanism, and matters because a badly named required argument is the first thing a consumer meets:
- target: $.paths["/invoices/{invoiceId}"].get.parameters[0]
update:
x-speakeasy-name-override: invoiceId # not "id0" or "pathParam1"
4. Apply the overlay in the generation pipeline
Merge, then generate, so no generator ever reads the unmodified document:
# Produce the effective document the generators will consume.
speakeasy overlay apply \
--overlay overlays/sdk-naming.yaml \
--schema openapi/api.bundled.yaml \
--out dist/api.sdk.yaml
# Every language generates from the same merged document.
for lang in typescript python go; do
speakeasy generate sdk --lang "$lang" --schema dist/api.sdk.yaml --out "sdks/$lang"
done
Confirm the merge did what you intended before generating anything expensive:
yq -r '.paths["/invoices"].get["x-speakeasy-name-override"]' dist/api.sdk.yaml
list
5. Assert the emitted surface in CI
Method names are public API. Snapshot them so a change cannot happen by accident:
#!/usr/bin/env bash
# scripts/check-sdk-surface.sh — the public method list is a contract.
set -euo pipefail
speakeasy overlay apply --overlay overlays/sdk-naming.yaml \
--schema openapi/api.bundled.yaml --out /tmp/api.sdk.yaml
speakeasy generate sdk --lang typescript --schema /tmp/api.sdk.yaml --out /tmp/sdk
# Extract the public surface as "namespace.method", sorted and stable.
grep -roP '(?<=async )\w+(?=\()' /tmp/sdk/src/sdk/*.ts \
| sed -E 's|.*/([a-z]+)\.ts:|\1.|' | sort -u > /tmp/surface.txt
if ! diff -u sdk-surface.txt /tmp/surface.txt; then
echo >&2
echo 'ERROR: the generated SDK surface changed.' >&2
echo ' A rename is a MAJOR version bump for every consumer.' >&2
echo ' If intended, update sdk-surface.txt in this commit.' >&2
exit 1
fi
echo "OK SDK surface unchanged ($(wc -l < /tmp/surface.txt) methods)"
OK SDK surface unchanged (34 methods)
Complete Working Example
The generation job, with the overlay applied and the surface asserted before anything is published:
# .github/workflows/sdks.yml
name: SDKs
on:
pull_request:
paths: ['openapi/**', 'overlays/**']
push:
tags: ['v*.*.*']
jobs:
sdks:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with: { node-version: '20', cache: 'npm' }
- run: npm ci
- name: Bundle the contract
run: npx @redocly/cli@2 bundle openapi/api.yaml -o openapi/api.bundled.yaml
# Naming lives only in the overlay — the contract is never edited for the SDK.
- name: Apply the naming overlay
run: |
speakeasy overlay apply \
--overlay overlays/sdk-naming.yaml \
--schema openapi/api.bundled.yaml \
--out dist/api.sdk.yaml
# An overlay target that no longer matches is a silent no-op — catch it.
- name: Verify every overlay action matched
run: |
expected=$(yq -r '.actions | length' overlays/sdk-naming.yaml)
applied=$(yq -r '[.paths[][]? | select(.["x-speakeasy-name-override"])] | length' \
dist/api.sdk.yaml)
echo "overlay actions: ${expected}, applied overrides: ${applied}"
[ "${applied}" -ge "${expected}" ] || {
echo '::error::Some overlay actions did not match — a path or method was renamed.'
exit 1
}
- name: Generate the SDKs
run: |
for lang in typescript python go; do
speakeasy generate sdk --lang "$lang" --schema dist/api.sdk.yaml --out "sdks/$lang"
done
# THE GATE — the public method list may not change without an explicit commit.
- name: Assert the SDK surface
run: ./scripts/check-sdk-surface.sh
- name: Publish on a tag
if: startsWith(github.ref, 'refs/tags/v')
run: speakeasy run --publish
env:
SPEAKEASY_API_KEY: ${{ secrets.SPEAKEASY_API_KEY }}
Expected output on a change that touches only the contract:
overlay actions: 12, applied overrides: 12
OK SDK surface unchanged (34 methods)
The “verify every overlay action matched” step is the one worth copying even if you use a different generator. An overlay whose JSONPath target no longer resolves does not error — it simply does nothing, and the next release quietly ships a client with the old machine-derived names for those operations. Comparing the number of actions against the number of applied overrides turns that silent failure into a build error.
Gotchas & Edge Cases
A rename is a breaking change for the SDK. The generated method name is public API for every consumer, so renaming one is a major version bump even though the contract did not change at all. This is exactly the case covered by Semver policy for OpenAPI-driven SDKs — get the naming right before the first 1.0.0, because afterwards each improvement costs consumers a migration.
Overlay targets are JSONPath against the bundled document. A target written against the split source will not match after bundling, and the mismatch is silent. Always apply the overlay to the same bundled artifact the generators consume.
Group names are namespaces, so they collide with method names. A group called list and a method called list at the same level produce invalid code in some languages and confusing code in others. Keep groups as plural nouns and methods as verbs.
Language idioms differ even from identical instructions. The same grouping produces nested clients in TypeScript and Python, and packages or nested structs elsewhere. Review the generated surface in at least two languages before settling the names, because a grouping that reads well in one can be awkward in another.
Naming decisions belong in review, not in a generator flag. Because the overlay is a small file of explicit intentions, a pull request that changes it is readable and the reasoning can be recorded next to it. Scattering the same decisions across generator configuration per language makes them invisible and lets the languages drift apart.
The cost of a rename rises sharply once an SDK is published, which is why the naming work belongs before the first stable release:
FAQ
Why not just edit the OpenAPI document directly?
Because naming is an SDK concern, and the specification may be owned by another team or shared across services. An overlay keeps the contract clean, avoids changing documentation URLs derived from operationId, and makes every naming decision reviewable in one small file.
Does renaming a method break consumers?
Yes. A generated method name is public API for anyone using the SDK, so a rename is a major version bump regardless of whether the underlying contract changed. Settle the naming before the first stable release.
Do namespaces work the same in every language?
The grouping is the same but the idiom differs — nested clients in TypeScript and Python, packages or nested structs elsewhere. Define the grouping once and let each generator express it naturally, then review the result in more than one language.
Related
- Speakeasy — the parent guide and the wider generation workflow.
- Automating SDK releases with the Speakeasy GitHub Action — the release pipeline this fits into.
- Semver policy for OpenAPI-driven SDKs — why a rename is a major bump.
- Splitting and bundling OpenAPI with Redocly CLI — producing the bundled document overlays target.