If you haven’t read the previous article on the importance of design patterns, I suggest you start there before reading this one. If however you’re already convinced on design patterns, read on to see my top picks for some examples we use at Stripe.
You might disagree with how the Stripe API is designed, and the design you end up with is likely going to be different than what we use. That’s just fine, since different companies have different use cases. Instead I present here some design patterns that I believe are generic enough to be useful for just about anyone in the API design process.
Language
Naming things is hard. This is true of most things in computer science, and API design is no different. The problem here is that similar to variable and function names, you want API routes, fields and types that are clear, yet concise. This is hard to do at the best of times, but here are a few tips.
Use simple language
This one’s an obvious suggestion, but in practice it’s actually quite hard to do and the cause of much bike-shedding. Try to distill a concept down to its core and don’t be afraid to use a thesaurus for synonyms.
For example, when building a platform don’t confuse the concept of a user and a customer. A user (at least in Stripe terms) is a party that directly uses your platform’s product, a customer (also known as an “end-user”) is the party who ends up purchasing the goods or services that your user might be offering. You don’t have to use those exact terms (‘user’ and ‘end-user’ are perfectly fine), as long as you’re consistent with your language.
Avoid jargon
Industries come with their own jargon; don’t assume that your user knows everything there is to know about your specific industry. As an example, the 16 digit number you see on your credit card is called the Primary Account Number, or PAN for short. When running in fintech circles it’s normal to have people talk about PANs, DPANs and FPANs, so you’d be forgiven for doing something like this in your payments API:
card.pan = 4242424242424242;
Even if you know what PAN stands for, the connection between that and the 16 digit number on a credit card might still not be obvious. Instead avoid the jargon and use something more likely to be understood by a larger audience:
card.number = 4242424242424242;
This is especially relevant when thinking about who the audience of your API is. Likely the core role of the person implementing your API is that of a developer with no knowledge of fintech or any other specialized domain. It’s generally speaking better to assume that people aren’t familiar with the jargon of your industry.
Structure
Prefer enums over booleans
Let’s imagine that we have an API for a subscription model. As part of the API, we want users to be able to determine whether the subscription in question is active or if it has been canceled. It would seem reasonable to have the following interface:
Subscription.canceled={true, false}
This works, but say that some time after implementing the above, we decide to roll out a new feature: pausing subscriptions. A paused subscription means we’re taking a break from taking payments, but the subscription is still active and not canceled. To reflect this we could consider adding a new field:
Subscription.canceled={true, false}
Subscription.paused={true, false}
Now, in order to see the actual status of the subscription, we need to look at two fields rather than one. This also opens us up to more confusion: what if a subscription has canceled: true
and paused: true
? Can a subscription that has been canceled also be paused? Perhaps we consider that a bug and say that canceled subscriptions must have paused: false
.
Does that mean that the canceled subscription can be unpaused?
The problem only gets worse the more fields you add. Rather than being able to check a single value, you need a confusing stack of if/else statements to figure out exactly what’s going on with this subscription.
Instead, let’s consider the following pattern:
Subscription.status={"active", "canceled"}
A single field tells us in plain language what the status of the object is by using enums instead of booleans. Another upside is the extensibility and future-proofing that this technique gives us. If we go back to our previous example of adding a “pause” mechanic, all we need to do is add an additional enum:
Subscription.status={"active", "canceled", "paused"}
We’ve added functionality but kept the complexity of the API at the same baseline whilst also being more descriptive. Should we ever decide to remove the subscription pausing functionality, removing an enum is always going to be easier than removing a field.
This doesn’t mean that you should never use booleans in your API, as there are almost certainly edge cases where they make more sense. Instead I urge you to consider before adding them the future possibility where boolean logic no longer makes sense (e.g., having a third option).
Use nested objects for future extensibility
A follow on from the previous tip: try to logically group fields together. The following:
customer.address = {
line1: "Main Street 123",
city: "San Francisco",
postal_code: "12345"
};
is much cleaner than:
customer.address_line1 = "Main street 123";
customer.address_city = "San Francisco";
customer.address_postal_code: "12345";
The first option makes it much easier to add an additional field later (e.g., a country
field if you decide to expand your business to overseas customers) and ensures that your field names don’t get too long. Keeping the top level of your resource nice and clean is not only preferable but soothes the soul as well.
Responses
Return the object type
In most cases, when you make an API call it’s to get or mutate some data. In the latter case the norm is to return a representation of the mutated resource. For example, if you update a customer’s email address, as part of your 200 response, you’d expect a copy of that customer with the new, updated email address.
To make life for developers easier, be explicit in what exactly is being returned. In the Stripe API, we have an object
field in the response that makes it abundantly clear what we’re working with. For example, the API route
/v1/customers/:customer/payment_methods/:payment_method
returns a PaymentMethod attached to a specific customer. It should hopefully be obvious from the route that you should expect a PaymentMethod back, but just in case, we include that object
field to make sure there can be no confusion:
{
"id": "pm_123",
"object": "payment_method",
"created": 1672217299,
"customer": "cus_123",
"livemode": false,
...
}
This helps a great deal when sifting through logs or adding some defensive programming to your integration:
if (response.data.object !== 'payment_method') {
// Not the expected object, bail
return;
}
Security
Use a permission system
Say you’re working on a new feature for your product dashboard, one that was specifically asked for by a large customer. You’re ready for them to test it out as a beta to get some feedback, so you let them know which route to make requests to and how to use it. The new route isn’t documented anywhere publicly and no one but your customer should even know about it, so you don’t worry too much.
A few weeks later, you push a change to the feature that addresses some of the feedback the large customer gave you, only for you to get a series of angry emails from other users asking why their integration has suddenly broken.
Disaster, it turns out that your secret API route has been leaked. Perhaps that initial customer got so excited about the new feature that they decided to tell their developer friends about it. Or perhaps that customer’s users had a look at their networking panel and saw these requests to an undocumented API and decided that they liked the look of that feature for their own product.
Not only do you have to clean up the current mess, but now your beta feature has effectively been dragged into a launched state. Since making any new changes will now require you to inform every user you have, your development velocity has slowed to a crawl.
Reverse engineering an API isn’t as difficult as you might think it is, and unless you take steps to prevent it, you can assume that people will.
Security through obscurity is the idea that something hidden is therefore secure. Just as this isn’t true for Christmas presents hidden in the closet, this isn’t true with web security. If you want to ensure that your private APIs stay private, make sure they can’t be accessed unless the user has the correct permissions. The easiest way to do this is to have a permission system tied to the API key. If the API key isn’t authorized to use the route, bail early and return an error message with status 403.
Make your IDs unguessable
I touched on this in my Object IDs post, but it’s worth revisiting here. If you are designing an API that returns objects with IDs associated with them, make sure those IDs can’t be guessed or otherwise reverse engineered. If your IDs are simply sequential, then you are at best inadvertently leaking information about your business that you might not want people to know and at worst creating a security incident in waiting.
To illustrate, if I make a purchase on your site and I get confirmation order ID of “10”, then I can make two assumptions:
- You don’t have nearly as much business as you probably claim
- I might be able to get information about the 9 previous orders (and all future ones) that I shouldn’t be able to, since I know their IDs
For that second assumption, I could try and find out more about your other customers by abusing your API in ways you didn’t intend:
// If the below route isn't behind a permission system,
// I can guess the ID and get potentially private
// information on your other customers
curl https://api.example.com/v1/orders/9
// Response
{
"id": "9",
"object": "order",
"name": "Lady Jessica",
"email": "jessica@benegesserit.com",
"address": "1 Palace Street, Caladan"
}
Instead, make your IDs unguessable by for instance using UUIDs. Using what is essentially a string of random numbers and letters as an ID means there’s no way to guess what the next ID looks like based on the one you have.
What you lose in convenience (it’s much easier to talk about “order 42” than “order 123e4567-e89b-12d3-a456-426614174000”) you’ll make up for in security benefits. Don’t forget to make it human readable by adding object prefixes though, generating your order IDs in the format order_3LKQhvGUcADgqoEM3bh6pslE will make your and the humans who build with your API’s lives easier.
Designing APIs for humans
There are many resources out there for how you should design your API, and I hope that this article gave you some food for thought and the incentive to dive deeper into this rabbit hole.
At Stripe we take API design very seriously. Internally we have a design pattern document containing what I’ve written about above and much more. It includes examples of good and bad design, notable exceptions and even a how-to guide on adding things like enums to existing resources. My favorite part is the “Discouraged” section, where examples of questionable design that exist in our API today are highlighted as a warning to future Stripe developers.
If you enjoyed this, check out the other articles in the Designing APIs for humans series. I also recommend joining the APIs you won’t hate community for more thoughts on API design.
About the author
Paul Asjes is a Developer Advocate at Stripe where he writes, codes and talks to developers. Outside of work he enjoys brewing beer, making biltong and losing to his son in Mario Kart.
Top comments (12)
Love this post! Would definitely welcome more along this line from the Stripe team!
Great series Paul, really loving these posts!
Love the concepts of this article.
QQ. While I understand the "hiding" how many customers we have via not exposing auto-incrementing PKs may be important to business people, don't these two statements directly conflict?
Given you have secured your Api isn't that a moot point about exposing PKs? Any special knowledge of a system is as you first pointed out inconsequential in a well-built/designed/implemented Api right? Our security makes special knowledge about a system DOA?
The non-autoincrementing PKs only seem to be useful in an insecure Api strictly, almost like a crutch?
Thanks for the article and tips!
Fun fact. When working with Stripe API, you come across a ton of unnecessary empty fields. When you just want to get the same information as on the payment page, you have to study a bunch of nested objects.
The calculated amount of VAT for the current payment, for example, you will not receive anywhere at all! This is the official answer from support. This information is simply not available anywhere, all you can do is subtract the paid amount from the cost of the product.
Great architecture, great docs page, great articles. But awful experience in real life.
Thanks for the feedback! Can you provide examples of unnecessary empty fields? I'd like to flag those to the team.
It's true that you can't get calculated VAT directly from a PaymentIntent, we actually recommend that you store the Tax Transaction ID as metadata on your PaymentIntent so you can retrieve it later.
In your case however it appears that you're using Checkout Sessions, where you can get the tax amount after the fact, although you'll need to expand the
breakdown
field to get the details.This level of HTTP based API is on level of web 10 year before. Like using POST to update data, it's already overdue. Flat response object it's not descriptive enough. There is no content negotiations. Your human approach heavily depends on excellent documentation. It's definitely somewhere around level 1 on RMM. I agree on UUID, but adding prefix doesn't seems right. It carries noticable complexity to backend to use uuid on DAL and then enchant ID with prefix. Which appears in log at what form? There was good comment, that this approach make business manager happy. It's trivial, and business oriented. But there is much more API could and should do. For example for each API release, even for minor, must be code review on client side, to check what was added/changed and find what a data means in documentation. The message could be more self descriptive. This is definitely good start, but for good API it has a lot of work ahead.
Though it is interesting
stripe documentation team can learn from you, FYI great artical.
awesome