Designing Type-Safe APIs for Multiple Languages
One OpenAPI spec generates SDKs in a single typed language across the stack, Python, and Go. Here is how we keep type safety tight across languages while adding hand-written convenience layers.

We ship official SDKs in a single typed language across the stack, Python, and Go. They all start from the same OpenAPI specification. They all end with the same developer experience: type-safe, auto-completing, impossible-to-misuse API calls that work on the first try.
Getting there was harder than it sounds. Auto-generating SDKs from a spec gives you coverage. Hand-writing SDKs gives you quality. We do both, and the tension between them is where the interesting design decisions live.
The SDK dilemma

Pure auto-generation produces SDKs that are technically correct but painful to use. The method names are verbose. The types are overly nested. The error handling follows whatever pattern the generator chose, not whatever pattern is idiomatic in the target language. Developers can feel the difference between a generated SDK and one that was crafted for their language.
Pure hand-writing produces SDKs that feel native but are expensive to maintain. Every API change requires manual updates to three SDKs. The risk of a type mismatch between the API and the SDK grows with every change. Eventually, hand-written SDKs drift from the actual API behavior, and developers start encountering bugs that exist only in the SDK, not in the API itself.
We split the difference. The types, request builders, and HTTP clients are auto-generated from the OpenAPI spec. The convenience methods, error handling, and developer-facing API surface are hand-written on top. This gives us the consistency of code generation with the ergonomics of hand-crafted code.
API products with official SDKs in three or more languages see roughly 40% higher adoption than products that offer only raw HTTP endpoints. The SDK removes friction. It handles authentication, retries, rate limiting, and response parsing so the developer can focus on their own product logic.
OpenAPI as the single source of truth
Every type, endpoint, parameter, and response in our API is defined in an OpenAPI specification that lives alongside the implementation code. When an engineer adds a new endpoint, they update the spec first, then implement the handler. The spec is not a byproduct of the code — it is the contract that the code fulfills.
This ordering matters. When the spec comes first, the SDK generation pipeline picks up changes immediately. CI runs the generator, produces updated SDK code, runs the SDK test suites, and flags any issues before the API change ships. The spec, the implementation, and the SDKs stay in sync on every commit.
We validate the OpenAPI spec in CI against the actual API behavior. Integration tests make real API calls through the SDK and verify that the responses match the spec. If the spec says a field is a string and the API returns a number, the test fails. This catches drift between the spec and the implementation before it reaches developers.
Language-specific idioms

The generated code is language-agnostic in structure. The hand-written convenience layer is where we make each SDK feel native.
In a single typed language across the stack, we use full generics so that `nowah.flights.search()` returns a properly typed `FlightSearchResponse` without any casting. The SDK exports types for every request and response so developers get auto-completion in their editor. Error handling uses typed exceptions that extend a base `NowahError` class.
const nowah = new NowahClient({ apiKey: 'nwh_...' });
const flights = await nowah.flights.search({
origin: 'JFK',
destination: 'CDG',
departureDate: '2026-06-15',
passengers: { adults: 2 },
cabinClass: 'economy',
});
// flights.offers is typed as FlightOffer[]
// flights.nextCursor is typed as string | nullIn Python, we use type hints and dataclasses. Response objects are proper Python objects with attribute access, not dictionaries. The SDK supports both synchronous and asynchronous usage, because some Python developers work in async frameworks and others do not. We follow PEP 8 naming conventions (snake_case methods, not camelCase) even though the API uses camelCase — the SDK translates transparently.
In Go, we use structs with JSON tags. Response handling is idiomatic Go with explicit error returns rather than exceptions. The SDK provides a functional options pattern for client configuration, which is the convention Go developers expect.
Each language's SDK feels like it was written by someone who primarily works in that language. That is the goal. A a single typed language across the stack developer should not feel like they are using a Java SDK that was transpiled. A Go developer should not feel like they are using a Python SDK with different syntax.
Testing generated code
Every API change triggers a pipeline that regenerates all three SDKs and runs their test suites. The tests make real API calls against our sandbox environment and validate response types, error handling, and edge cases.
This catches a specific class of bugs: spec changes that are valid in one language but produce invalid code in another. For example, adding an optional field with a default value works fine in a single typed language across the stack and Python but requires specific handling in Go where zero values and absent values are different concepts.
We run the full SDK test suite on every pull request that touches the API spec. The CI pipeline regenerates the SDK, installs it in a fresh project, and runs integration tests. If any SDK's tests fail, the pull request cannot merge. This gate ensures that no API change ships that breaks the SDK experience in any language.
The convenience layer
The auto-generated code handles the mechanics: HTTP transport, authentication headers, request serialization, response deserialization. The hand-written convenience layer provides the API surface that developers actually interact with.
The convenience layer wraps raw API calls with methods like `nowah.flights.search()` instead of `nowah.post('/flights/search', body)`. It adds automatic retries with exponential backoff for transient failures. It handles rate limit responses by reading the `Retry-After` header and waiting before retrying. It provides pagination helpers that abstract away cursor management.
These are the features that make an SDK feel professional rather than mechanical. Auto-generated code does not know that rate limits should be retried with a delay, or that cursor pagination should be wrapped in an iterator, or that certain error types are retryable and others are not. The hand-written layer encodes this domain knowledge.
Versioning SDKs independently from the API
SDK versions and API versions are not the same. The API might be on version 2025-01-15, but the a single typed language across the stack SDK might be on version 3.2.1. This is intentional.
SDK versions change when the SDK code changes — new convenience methods, bug fixes, dependency updates. API versions change when the API contract changes — new endpoints, changed response shapes, deprecated fields. These happen on different timelines.
We follow semantic versioning for SDKs. Patch versions (3.2.1 to 3.2.2) are bug fixes. Minor versions (3.2.0 to 3.3.0) add features without breaking changes. Major versions (3.0.0 to 4.0.0) introduce breaking changes to the SDK surface.
An SDK major version bump usually corresponds to an API version bump, but not always. Sometimes we restructure the SDK surface for ergonomic reasons without any API change. When that happens, we publish a migration guide specific to the SDK.
This independence lets us ship SDK improvements quickly without waiting for API changes, and it lets us support multiple API versions from a single SDK version.
Type safety across multiple languages is not a solved problem. It is a practice that requires investment in tooling, testing, and the willingness to maintain hand-written code alongside generated code. The payoff is that developers in each language get an SDK that feels native, stays accurate, and handles the hard parts so they can focus on their travel product.
Nowah is an AI travel agent that searches and books real flights and hotels through conversation — no filters, no thirty open tabs. Plan your next trip.