We launched SDKs—here’s how we’ll keep them correct

Sri Raghavan

First of all, let’s get the big announcement out of the way: starting today, SDKs for Orb are available in Python, Node.js/Typescript, and Go. We’re pretty excited about the ergonomic wins these SDKs bring when it comes to integrating with Orb. They offer a wide range of benefits including autocomplete and static checking, retries, idempotency support, and more!

Better yet, if an Orb SDK isn’t available in your language of choice, our OpenAPI spec can help you get up and running with one quickly!

Before we dive deeper into what led us on this path…

Why invest in an OpenAPI spec?

As a billing data platform, our web API is the primary surface area that technical folks interact with; compared to raw cURL commands, SDKs in your language of choice can lead to a much better developer experience, and make it quicker to get up and running. Our API is ever-evolving and handwritten SDKs could fall out-of-date (especially in languages we don’t use regularly in-house).

An increasingly common approach to this problem is as follows:

  • Produce an “API spec” (over the last many years, OpenAPI has emerged dominant in this space) that accurately describes the various operations in the API, and what each takes in as input and produces as output
  • Use that spec to produce a language-native SDK, that takes care of marshaling and unmarshaling HTTP content, error handling, and other concerns that often become repetitive when directly calling HTTP endpoints

Additionally, OpenAPI specs provide for another important need - developer documentation! While it’s possible to read through the code of open-source SDK, it’s often not the quickest or most pleasant way to determine “how to use this endpoint”, “what does this endpoint return”, etc. OpenAPI specs can be used to produce high-quality documentation meant to be consumed by humans.

Accuracy is critical

OpenAPI specs are great - you can turn them into SDKs in your language of choice, and you can use them to produce great documentation. But how useful a spec is depends directly on how accurate it is - if endpoints are added to the API without being added to the spec, or if fields are typed incompletely or incorrectly, any artifacts downstream of the spec will retain those issues. Trust is paramount - if the spec isn’t consistently accurate, developer confidence is quickly undermined, and no engineer likes to play guess-and-check when integrating business-critical APIs.

But how do we make sure the spec is (and stays) correct?

This is a tale as old as time - if you have multiple sources of truth, they may disagree (and over time, it’s likely that they do). Solution? Consolidate to a single source of truth!

There are roughly two options here for consolidating “API shape” into a single source of truth:

  1. Designate the spec as the source of truth, and produce code (or maybe types) that need to be implemented. Often, this can be done by generating types from the spec, and statically checking that the code conforms to those types
  2. Designate the code as the source of truth, and generate a spec from it. This requires that the code is strictly enough typed that introspection can produce reasonable types in the first place

How the Orb team produces a guaranteed-correct OpenAPI spec

At Orb, we use Python and Flask for our backend and APIs. The last few versions of Python have introduced increasingly robust support for static typing, and this plays a significant role in our ability to maintain API shape correctness. We can use a static type-checker like Mypy to verify that our request-handling code handles the inputs it receives. Additionally (unlike Typescript, and other languages that completely elide types at runtime), Python’s type system has powerful runtime introspection capabilities - generating a JSON schema for a Python function is as simple as importing the function, accessing its types via typing.get_type_hints, and transforming the result into valid JSON.

Pydantic is a powerful library that links together parsing/validation and serialization with the Python type system and automatically handles marshaling between JSON and Python types. Using Pydantic, we can declare endpoint inputs as statically typed Python classes, and then a decorator that understands those types can validate the JSON input against them (automatically raising HTTP 400 invalid-request errors as required), and pass the resulting instance of the request params into the handler function. Pydantic, additionally, includes functionality for producing valid JSONSchema types for declared models - we can leverage this later to construct the OpenAPI spec fragment for each operation.

Readers familiar with the Python ecosystem will note that this is very similar to the FastAPI layer on top of Starlette - we considered migrating to FastAPI, but the costs of migration (in particular, rewriting code to use Starlette patterns and primitives vs those of Flask) were likely too great to make this approach feasible.

Additionally, we can use the same decorator (@router.route above) to declare important OpenAPI-related metadata about the endpoint: the operation_id, tags, and more. We can even use Python’s standard patterns for documentation to describe the endpoint - when we’re generating the OpenAPI spec, we can pull this documentation in as its description.

At this point, producing an OpenAPI spec is as simple as importing our router to get the set of endpoints, collecting the relevant input and output types and generating a JSONSchema for each, and producing an operation for each endpoint with the appropriate metadata (including references to those input and output types).

What can we do with OpenAPI spec generation?

We can wire up our spec generator into our CI/CD pipeline so that we’re automatically publishing a matching version of our spec when we deploy new API code. (This is what you see at https://docs.withorb.com/spec.json)

Additionally, via Docusaurus, we can produce up-to-date documentation from our spec (also updated at PR and deploy time). First, an obvious massive boon: our docs are automatically kept up-to-date with our API as it’s deployed. Second, this allows for an even better ergonomic experience for our engineers - add a new endpoint, or modify types for an existing one, and you’ll automatically get a Vercel preview deployment that shows the resulting docs changes.

We transform our OpenAPI spec into a Postman collection as well, allowing any engineer to “fork and go” to explore our APIs without downloading a thing.

Last, but certainly not least…

Idiomatic SDKs powered by Stainless

With Stainless and our OpenAPI spec, we can produce idiomatic SDKs in various languages. Python, Node.JS/Typescript, and Go are available today, with Java coming soon - let us know if there’s a language you’d like to see an Orb SDK for!

Because Stainless and our OpenAPI spec are wired into our CI/CD pipeline, our SDKs update automatically, so it’s easier than ever for our customers to start using new API features.

Orb’s SDKs have type safety built-in - autocomplete makes your life easier as an engineer, and static checking helps verify inputs and outputs are being handled correctly!

Orb’s SDKs handle the important, but sometimes-tedious parts of building a reliable API integration:

  • our request idempotency support is built in and means you'll never have to worry about double-subscribing
  • retries are built into the SDK - transient errors will be dealt with automatically (in concert with idempotency support)
  • the SDK manages request timeouts gracefully as well
  • error-handling is rich and idiomatic

Stainless’ SDKs come with other ergonomic niceties in various languages:

  • In Python, response types are Pydantic objects, so you can access properties with . access, the same way you might with dataclasses, while request params are passed as keyword arguments (avoiding boilerplate - though you can import the request params’ corresponding TypedDict and construct that explicitly for even more type safety)
  • List requests can be paginated automatically - no manual handling of cursors or pages, a paginated request can be as simple as for customer in client.customers.list(): ...

posted:
October 24, 2023
Category:
Eng Deep Dive

Ready to solve billing?

Contact us to learn how you can revamp your billing infrastructure today.

Let's talk.

Please enter a valid work email
Thank you! We'll be in touch shortly.
Oops! Something went wrong while submitting the form.