API Design Patterns
By JJ Geewax
5/5
()
About this ebook
API Design Patterns lays out a set of design principles for building internal and public-facing APIs.
In API Design Patterns you will learn:
Guiding principles for API patterns
Fundamentals of resource layout and naming
Handling data types for any programming language
Standard methods that ensure predictability
Field masks for targeted partial updates
Authentication and validation methods for secure APIs
Collective operations for moving, managing, and deleting data
Advanced patterns for special interactions and data transformations
API Design Patterns reveals best practices for building stable, user-friendly APIs. These design patterns can be applied to solve common API problems and flexibly altered to fit specific needs. Hands-on examples and relevant cases illustrate patterns for API fundamentals, advanced functionalities, and uncommon scenarios.
Foreword by Jon Skeet.
Purchase of the print book includes a free eBook in PDF, Kindle, and ePub formats from Manning Publications.
About the technology
APIs are contracts that define how applications, services, and components communicate. API design patterns provide a shared set of best practices, specifications and standards that ensure APIs are reliable and simple for other developers. This book collects and explains the most important patterns from both the API design community and the experts at Google.
About the book
API Design Patterns lays out a set of principles for building internal and public-facing APIs. Google API expert JJ Geewax presents patterns that ensure your APIs are consistent, scalable, and flexible. You’ll improve the design of the most common APIs, plus discover techniques for tricky edge cases. Precise illustrations, relevant examples, and detailed scenarios make every pattern clear and easy to understand.
What's inside
Guiding principles for API patterns
Fundamentals of resource layout and naming
Advanced patterns for special interactions and data transformations
A detailed case-study on building an API and adding features
About the reader
For developers building web and internal APIs in any language.
About the author
JJ Geewax is a software engineer at Google, focusing on Google Cloud Platform, API design, and real-time payment systems. He is also the author of Manning’s Google Cloud Platform in Action.
Table of Contents
PART 1 INTRODUCTION
1 Introduction to APIs
2 Introduction to API design patterns
PART 2 DESIGN PRINCIPLES
3 Naming
4 Resource scope and hierarchy
5 Data types and defaults
PART 3 FUNDAMENTALS
6 Resource identification
7 Standard methods
8 Partial updates and retrievals
9 Custom methods
10 Long-running operations
11 Rerunnable jobs
PART 4 RESOURCE RELATIONSHIPS
12 Singleton sub-resources
13 Cross references
14 Association resources
15 Add and remove custom methods
16 Polymorphism
PART 5 COLLECTIVE OPERATIONS
17 Copy and move
18 Batch operations
19 Criteria-based deletion
20 Anonymous writes
21 Pagination
22 Filtering
23 Importing and exporting
PART 6 SAFETY AND SECURITY
24 Versioning and compatibility
25 Soft deletion
26 Request deduplication
27 Request validation
28 Resource revisions
29 Request retrial
30 Request authentication
JJ Geewax
JJ Geewax is a software engineer at Google, focusing on Google Cloud Platform, API design, and real-time payment systems. He is also the author of Manning’s Google Cloud Platform in Action.
Related to API Design Patterns
Related ebooks
The Design of Web APIs Rating: 0 out of 5 stars0 ratingsMicro Frontends in Action Rating: 0 out of 5 stars0 ratingsGraphQL in Action Rating: 2 out of 5 stars2/5Functional Programming in JavaScript: How to improve your JavaScript programs using functional techniques Rating: 0 out of 5 stars0 ratingsTesting JavaScript Applications Rating: 5 out of 5 stars5/5RESTful API Design - Best Practices in API Design with REST: API-University Series, #3 Rating: 5 out of 5 stars5/5Go in Practice Rating: 5 out of 5 stars5/5Operations Anti-Patterns, DevOps Solutions Rating: 0 out of 5 stars0 ratingsData-Oriented Programming: Reduce software complexity Rating: 4 out of 5 stars4/5Entity Framework Core in Action Rating: 0 out of 5 stars0 ratingsFunctional Programming in C#, Second Edition Rating: 0 out of 5 stars0 ratingsDesigning APIs with Swagger and OpenAPI Rating: 0 out of 5 stars0 ratingsEntity Framework Core Cookbook - Second Edition Rating: 0 out of 5 stars0 ratingsInfrastructure as Code, Patterns and Practices: With examples in Python and Terraform Rating: 0 out of 5 stars0 ratingsSeriously Good Software: Code that works, survives, and wins Rating: 5 out of 5 stars5/5Good Code, Bad Code: Think like a software engineer Rating: 5 out of 5 stars5/5Microservices in Action Rating: 0 out of 5 stars0 ratingsHTTP/2 in Action Rating: 0 out of 5 stars0 ratingsAPI Security in Action Rating: 5 out of 5 stars5/5Docker in Action, Second Edition Rating: 3 out of 5 stars3/5Microservices Patterns: With examples in Java Rating: 5 out of 5 stars5/5Five Lines of Code: How and when to refactor Rating: 0 out of 5 stars0 ratingsBootstrapping Microservices with Docker, Kubernetes, and Terraform: A project-based guide Rating: 3 out of 5 stars3/5Node.js in Action Rating: 0 out of 5 stars0 ratingsServerless Architectures on AWS: With examples using AWS Lambda Rating: 0 out of 5 stars0 ratingsMicroservices Security in Action Rating: 0 out of 5 stars0 ratingsSecure by Design Rating: 4 out of 5 stars4/5The Programmer's Brain: What every programmer needs to know about cognition Rating: 5 out of 5 stars5/5Unit Testing Principles, Practices, and Patterns Rating: 4 out of 5 stars4/5Kubernetes Native Microservices with Quarkus and MicroProfile Rating: 0 out of 5 stars0 ratings
Internet & Web For You
Grokking Algorithms: An illustrated guide for programmers and other curious people Rating: 4 out of 5 stars4/5Everybody Lies: Big Data, New Data, and What the Internet Can Tell Us About Who We Really Are Rating: 4 out of 5 stars4/5Notion for Beginners: Notion for Work, Play, and Productivity Rating: 4 out of 5 stars4/5The $1,000,000 Web Designer Guide: A Practical Guide for Wealth and Freedom as an Online Freelancer Rating: 4 out of 5 stars4/5Coding All-in-One For Dummies Rating: 4 out of 5 stars4/5Python: Learn Python in 24 Hours Rating: 4 out of 5 stars4/5UX/UI Design Playbook Rating: 4 out of 5 stars4/5Coding For Dummies Rating: 5 out of 5 stars5/5Get Into UX: A foolproof guide to getting your first user experience job Rating: 4 out of 5 stars4/5Cybersecurity For Dummies Rating: 5 out of 5 stars5/5Principles of Web Design Rating: 0 out of 5 stars0 ratingsLearn JavaScript in 24 Hours Rating: 3 out of 5 stars3/5LinkedIn Profile Optimization For Dummies Rating: 4 out of 5 stars4/5The Basics of User Experience Design by Interaction Design Foundation Rating: 4 out of 5 stars4/5Clean Code in JavaScript: Develop reliable, maintainable, and robust JavaScript Rating: 5 out of 5 stars5/5Explain the Cloud Like I’m 10 Rating: 5 out of 5 stars5/5The Logo Brainstorm Book: A Comprehensive Guide for Exploring Design Directions Rating: 4 out of 5 stars4/5The Designer’s Guide to Figma: Master Prototyping, Collaboration, Handoff, and Workflow Rating: 0 out of 5 stars0 ratingsCoding with AI For Dummies Rating: 0 out of 5 stars0 ratingsJavaScript: Beginner's Guide to Programming Code with JavaScript Rating: 5 out of 5 stars5/5The Gothic Novel Collection Rating: 5 out of 5 stars5/5Stop Asking Questions: How to Lead High-Impact Interviews and Learn Anything from Anyone Rating: 5 out of 5 stars5/5The Digital Marketing Handbook: A Step-By-Step Guide to Creating Websites That Sell Rating: 5 out of 5 stars5/5Social Engineering: The Science of Human Hacking Rating: 3 out of 5 stars3/5Learn NodeJS in 1 Day: Complete Node JS Guide with Examples Rating: 3 out of 5 stars3/5Making Money By Selling 3D Models Online Rating: 5 out of 5 stars5/52022 Adobe® Premiere Pro Guide For Filmmakers and YouTubers Rating: 5 out of 5 stars5/5
Reviews for API Design Patterns
1 rating0 reviews
Book preview
API Design Patterns - JJ Geewax
Part 1 Introduction
API design is complicated. After all, if it were easy there would probably be little need for this book at all. But before we start exploring the tools and patterns to make API design a bit more manageable, we first have to agree on some fundamental terminology and what to expect from this book. In the next two chapters, we’ll cover some of the introductory material that will act as a platform for us to build on throughout the rest of the book.
We’ll start in chapter 1 by defining in detail what we mean by an API. More importantly, we’ll investigate what good APIs look like and how to distinguish them from bad APIs. Then in chapter 2, we’ll look more closely at what we mean by a design pattern and the anatomy of the patterns outlined in the rest of the book, with the end goal of relying on these design patterns to build consistently good APIs.
1 Introduction to APIs
This chapter covers
What are interfaces?
What are APIs?
What is resource orientation?
What makes an API good
?
Chances are that by picking up this book you’re already familiar with the high-level concept of an API. Additionally, you probably already know that API stands for application programming interface, so the focus of this chapter will cover what these basics actually mean in more detail, as well as why they matter. Let’s start by looking more closely at this idea of an API.
1.1 What are web APIs?
An API defines the way in which computer systems interact. And since an exceptionally small number of systems live in a vacuum, it should come as no surprise that APIs are everywhere. We can find APIs in the libraries we use from language package managers (e.g., an encryption library that provides a method like function encrypt(input: string): string) and technically in the code we write ourselves, even if it’s never intended for use by anyone else. But there’s one special type of API that is built to be exposed over a network and used remotely by lots of different people, and it’s these types that are the focus of this book, often called web APIs.
Web APIs are interesting in many ways, but the most interesting aspect of this special category is arguably the fact that those building the API have so much control while those using web APIs have relatively little. When we use a library, we deal in local copies of the library itself, meaning those building the API can do whatever they want, whenever they want without the possibility of harming users. Web APIs are different because there are no copies. Instead, when the builders of a web API make changes, these changes are forced on users whether they ask for them or not.
For example, imagine a web API call that allows you to encrypt data. If the team that works on this API decides to use a different algorithm when encrypting your data, you don’t really have a choice in the matter. When calling the encryption method, your data will be encrypted with the latest algorithm. In a more extreme example, the team could decide to shut off the API entirely and ignore your requests. At that point, your application will suddenly stop working and there’s not much you can do about it. Both of these scenarios are shown in figure 1.1.
Figure 1.1 Possible consumer-facing experiences when dealing with a web API
However, the drawbacks of a web API for consumers are often the primary benefits for those building the APIs: they’re able to maintain complete control of the API. For example, if the encryption API used a super-secret new algorithm, the team that built it would probably not want to just give that code away to the world in the form of a library. Instead, they’d probably prefer to use a web API, which would allow them to expose the functionality of the super-secret algorithm without giving away their valuable intellectual property. Other times, a system might require extraordinary computational power, which would take way too long to run if it was deployed as a library and run on a home computer or laptop. In those cases, such as with many machine learning APIs, building a web API allows you to expose the powerful functionality while hiding the computational requirements from consumers, shown in figure 1.2.
Figure 1.2 An example of a web API hiding the computational power needed
Now that we understand what APIs (and specifically web APIs) are, this raises the question: why do they matter?
1.2 Why do APIs matter?
It’s not uncommon for software to be designed and built for human use exclusively, and there’s nothing fundamentally wrong with this. However, over the past several years we’ve seen more and more focus on automation, where we aim to build computer programs that do what we humans do, only faster. Unfortunately, it’s at this point that the human-only
software becomes a bit of a problem.
When we design something exclusively for human use, where our interactions involve a mouse and keyboard, we tend to conflate the system’s layout and visual aspects with the raw data and functional aspects. This is a problem because it can be difficult to explain to a computer how to interact with a graphical interface. And this problem gets worse because changing the visual aspects of a program may also require us to reteach the computer how to interact with this new graphical interface. In effect, while changes may simply be cosmetic to us, they’re completely unrecognizable to a computer. Put differently, to a computer there is no such thing as cosmetic only.
APIs are interfaces specifically for computers with important properties to make it easy for computers to use them. For example, these interfaces have no visual aspects, so there’s no need to worry about superficial changes. And these interfaces generally evolve in only compatible
ways (see chapter 24), so there’s no need to reteach the computer anything in the face of new changes. In short, APIs provide a way to speak the language computers need to interact in a safe and stable way.
But this doesn’t stop at simple automation. APIs also open the door to composition, which allows us to treat functionality like Lego building blocks, assembling pieces together in novel ways to build things that are much larger than the sum of their parts. To complete the cycle, these new compositions of APIs can likewise join the ranks of reusable building blocks, enabling even more complex and extraordinary future projects.
But this leads to an important question: how can we make sure the APIs we build fit together like Lego bricks? Let’s start by looking at one strategy for this, called resource orientation.
1.3 What are resource-oriented APIs?
Many web APIs that exist today act a bit like servants: you ask them to do something and they go off and do it. For example, if we want the weather for our hometown we might order the web API to predictWeather(postalCode=10011)like a servant. This style of ordering another computer around by calling a preconfigured subroutine or method is often referred to as making a remote procedure call
(RPC) because we’re effectively calling a library function (or procedure) to be executed on another computer that is somewhere potentially far away (or remote). The critical aspect of APIs like this is the primary focus on the actions being performed. That is, we think about calculating the weather (predictWeather(postalCode=...)) or encrypting data (encrypt(data=...)) or sending an email (sendEmail(to=...)), each with an emphasis on doing
something.
So why aren’t all APIs RPC-oriented? One of the main reasons has to do with the idea of statefulness,
where API calls can either be stateful
or stateless.
An API call is considered stateless when it can be made independently from all other API requests, with no additional context or data whatsoever. For example, a web API call to predict the weather involves only one independent input (the postal code) and would therefore be considered stateless. On the other hand, a web API that stores a user’s favorite cities and provides weather forecasts for those cities has no runtime inputs but requires a user to already have stored the cities they’re interested in. As a result, this kind of API request, involving other prior requests or previously stored data, would be considered stateful. It turns out that RPC-style APIs are great for stateless functionality, but they tend to be a much poorer fit when we introduce stateful API methods.
NOTE If you happen to be familiar with REST, now might be a good time to point out that this section is not about REST and RESTful APIs specifically, but more generally about APIs that emphasize resources
(as most RESTful APIs do). In other words, while there will be a lot of overlap with the topic of REST, this section is a bit more general than just REST.
To see why this is, let’s consider an example of a stateful API for booking airline flights. In table 1.1, we can see a list of RPCs for interacting with airline travel plans, covering actions such as scheduling new bookings, viewing existing bookings, and canceling unwanted travel.
Table 1.1 Summary of methods for an example flight-booking API
Each of these RPCs is pretty descriptive, but there’s no escaping the requirement that we memorize these API methods, each of which is subtly different from the others. For example, sometimes a method talks about a flight
(e.g., RescheduleFlight()) and other times operates on a reservation
(e.g., CancelReservation()). We also have to remember which of the many synonymous forms of the action were used. For example, we need to remember whether the way to see all of our bookings is ShowFlights(), ShowAllFlights(), ListFlights(), or ListAllFlights() (in this case, it’s ShowAllFlights()). But what can we do to solve this? The answer comes in the form of standardization.
Resource orientation aims to help with this problem by providing a standard set of building blocks to use when designing an API in two areas. First, resource-oriented APIs rely on the idea of resources,
which are the key concepts we store and interact with, standardizing the things
that the API manages. Second, rather than using arbitrary RPC names for any action we can think of, resource-oriented APIs limit actions to a small standard set (described in table 1.2), which apply to each of the resources to form useful actions in the API. Thinking of this a bit differently, resource-oriented APIs are really just a special type of RPC-style APIs where each RPC follows a clear and standardized pattern:
Table 1.2 Summary of standard methods and their meanings
If we go down this route of special, limited RPCs, this means that instead of the variety of different RPC methods shown in table 1.1, we could come up with a single resource (e.g., FlightReservation) and get equivalent functionality with the set of standard methods, shown in table 1.3.
Table 1.3 Summary of standard methods applied to the flight resource
Standardization is clearly more organized, but does that mean that all resource-oriented APIs are strictly better than RPC-oriented APIs? Actually, no. For some scenarios RPC-oriented APIs will be a better fit (particularly in the case where the API method is stateless). In many other cases however, resource-oriented APIs will be much easier for users to learn, understand, and remember. This is because the standardization provided by resource-oriented APIs makes it easy to combine what you already know (e.g., the set of standard methods) with what you can easily learn (e.g., the name of a new resource) to start interacting with the API right away. Put a bit more numerically, if you are familiar with, say, five standard methods, then, thanks to the power of a reliable pattern, learning about one new resource is actually the same as learning five new RPCs.
Obviously, it’s important to note that not every API is the same, and it’s a bit crude to define the complexity of an API in terms of the size of a to-do list of stuff to learn.
On the other hand, there is an important principle at work here: the power of patterns. It seems that learning about composable pieces and combining them into more complex things that follow a set pattern tends to be easier than learning about pre-built complex things that follow a custom design every time. Since resource-oriented APIs exploit the power of battle-tested design patterns, they are often easier to learn and therefore better
than their RPC-oriented equivalents. But this brings us to an important question: What does better
mean here? How do we know if an API is good
? What does good
even mean?
1.4 What makes an API good
?
Before we explore a few of the different aspects that tend to make APIs good,
we first need to dig into why we have an API at all. In other words, what is the purpose of building an API in the first place? Often this comes down to two simple reasons:
We have some functionality that some users want.
Those users want to use this functionality programmatically.
For example, we might have a system that is amazing at translating text from one language to another. There are probably lots of people in the world who want this ability, but that alone isn’t enough. After all, we could launch a translation mobile app that exposes this amazing translation system instead of an API. To merit an API at all, the people who want this functionality must also want to write a program that uses it. Given these two criteria, where does this lead us when thinking about the desirable qualities of an API?
1.4.1 Operational
Starting with the most important piece, no matter what the final interface looks like, the system as a whole must be operational. In other words, it must do the thing users actually want. If this is a system that intends to translate text from one language to another, it must actually be able to do so. Additionally, most systems are likely to have many nonoperational requirements. For example, if our system translates text from one language to another, there may be nonoperational requirements related to things like latency (e.g., the translation task should take a few milliseconds, not a few days) or accuracy (e.g., translations should not be misleading). It’s these two aspects together that we say constitute the operational aspects of a system.
1.4.2 Expressive
If it’s important that a system is able to do something, it’s just as important that the interface to that system allows users to express the thing they want to do clearly and simply. In other words, if the system translates text from one language to another, the API should be designed such that there is a clear and simple way to do so. In this case, it might be an RPC called TranslateText(). This type of thing might sound obvious, but it can actually be more complicated than it seems.
One example of this hidden complication is the case where an API supports some functionality already but, due to an oversight on our part, we didn’t realize that users wanted it and therefore didn’t build an expressive way for users to access that functionality. Scenarios like these tend to manifest as workarounds, where users do unusual things to access hidden functionality. For example, if an API provides the ability to translate text from one language to another, then it’s possible a user could coerce the API into acting as a language detector even if they’re not really interested in translating anything, as shown in listing 1.1. As you might imagine, it would be far better if users had an RPC called DetectLanguage() rather than making lots of API calls guessing at the language.
Listing 1.1 Functionality to detect language using only a TranslateText API method
function detectLanguage(inputText: string): string { const supportedLanguages: string[] = ['en', 'es', ... ]; for (let language of supportedLanguages) { let translatedText = TranslateApi.TranslateText({ ❶ text: inputText, targetLanguage: language }); if (translatedText === inputText) { ❷ return language; } } return null; ❸ }
❶ This assumes the API in question defines a TranslateText method that takes some input text and a target language to translate into.
❷ If the translated text is the same as the input text, we know that the two languages are the same.
❸ If we don’t find translated text that is the same as the input text, we return null, indicating we can’t detect the language of the input text.
As this example shows, APIs that support certain functionality but don’t make it easy for users to access that functionality would be not very good. On the other hand, APIs that are expressive provide the ability for users to clearly dictate exactly what they want (e.g., translate text) and even how they want it done (e.g., within 150 milliseconds, with 95% accuracy
).
1.4.3 Simple
One of the most important things related to the usability of any system is simplicity. While it’s easy to argue that something being simple is reducing the number of things (e.g., RPCs, resources, etc.) in an API, unfortunately this is rarely the case. For example, an API could rely on a single ExecuteAction() method that handles all functionality; however, that’s not really simplifying anything. Instead, it shifts complexity from one place (lots of different RPCs) to another (lots of configuration in a single RPC). So what exactly does a simple API look like?
Rather than trying to excessively reduce the number of RPCs, APIs should aim to expose the functionality that users want in the most straightforward way possible, making the API as simple as possible, but no simpler. For example, imagine a translation API wanted to add the ability to detect the language of some input text. We could do this by returning the detected source text on the response of a translation; however, this still obfuscates the functionality by hiding it inside a method designed for another purpose. Instead, it would make more sense to create a new method that is designed specifically for this purpose, such as DetectLanguage(). (Note that we might also include the detected language when translating content, but that’s for another purpose entirely.)
Another common position on simplicity takes the old saying about the common case
(Make the common case fast
) but focuses instead on usability while leaving room for edge cases. This restatement is to make the common case awesome and the advanced case possible.
This means that whenever you add something that might complicate an API for the benefit of an advanced user, it’s best to keep this complication sufficiently hidden from a typical user only interested in the common case. This keeps the more frequent scenarios simple and easy, while still enabling more advanced features for those who want them.
For example, let’s imagine that our translation API includes the concept of a machine learning model to be used when translating text, where instead of specifying the target language, we choose a model based on the target language and use that model as the translating engine.
While this functionality provides much more flexibility and control to users, it is also much more complex, with the new common case shown in figure 1.3.
Figure 1.3 Translating text after choosing a model
As we can see, we’ve effectively made it much more difficult to translate some text in exchange for supporting the more advanced functionality. To see this more clearly, compare the code shown in listing 1.2 to the simplicity of calling TranslateText(Hello world
, es
).
Listing 1.2 Translating text after choosing a model
function translateText(inputText: string, targetLanguage: string): string { let sourceLanguage = TranslateAPI.DetectLanguage(inputText); ❶ let model = TranslateApi.ListModels({ ❷ filter: `sourceLanguage:${sourceLanguage} targetLanguage:${targetLanguage}`, })[0]; return TranslateApi.TranslateText({ text: inputText, modelId: model.id ❸ }); }
❶ Since we need to choose a model, we first need to know the language of the input text. To determine this we could rely on the hypothetical DetectLanguage() method provided by the API.
❷ Once we know both the source and destination languages, we can choose any of the matching models provided by the API.
❸ Now that we finally have all the required inputs, we can get back to our original goal of translating text into a target language.
How could we design this API to be as simple as possible, but no simpler as well as make the common case awesome and the advanced case possible? Since the common case involves users who don’t really care about a specific model, we could do this by designing the API so that it accepts either a targetLanguage or a modelId. The advanced case would still work (in fact, the code shown in listing 1.2 would continue to work), but the common case would look far simpler, relying on just a targetLanguage parameter (and expecting the modelId parameter to be left undefined).
Listing 1.3 Translating text to a target language (the common case)
function translateText(inputText: string, targetLanguage: string, modelId?: string): string { return TranslateApi.TranslateText({ text: inputText, targetLanguage: targetLanguage, modelId: modelId, }); }
Now that we have some background on how simplicity is an important attribute of a good
API, let’s look at the final piece: predictability.
1.4.4 Predictable
While surprises in our lives can sometimes be fun, one place surprises do not belong is in APIs, either in the interface definition or underlying behavior. This is a bit like the old adage about investing: If it’s exciting, you’re doing it wrong.
So what do we mean by unsurprising
APIs?
Unsurprising APIs rely on repeated patterns applied to both the API surface definition and the behavior. For example, if an API that translates text has a TranslateText() method that takes as a parameter the input content as a field called text, then when we add a DetectLanguage() method, the input content should be called text as well (not inputText or content or textContent). While this might seem obvious now, keep in mind that many APIs are built by multiple teams and the choice of what to call fields when presented a set of options is often arbitrary. This means that when two different people are responsible for these two different fields, it’s certainly possible that they’ll make different arbitrary choices. When this happens, we end up with an inconsistent (and therefore surprising) API.
Even though this inconsistency might seem insignificant, it turns out that issues like these are much more important than they appear. This is because it’s actually pretty rare that users of an API learn each and every detail by thoroughly reading all the API documentation. Instead, users read just enough to accomplish what they want to do. This means that if someone learns that a field is called text in one request message, they’re almost certainly going to assume it’s named the same way in another, effectively building on what they’ve already learned to make an educated guess about things they haven’t yet learned. If this process fails (e.g., because another message named that field inputText), their productivity hits a brick wall and they have to stop what they’re doing to go figure out why their assumptions failed.
The obvious conclusion is that APIs that rely on repeated, predictable patterns (e.g., naming fields consistently) are easier and faster to learn and therefore better. And similar benefits arise from more complex patterns, such as the standard actions we saw in our exploration of resource-oriented APIs. This brings us to the entire purpose of this book: APIs built using well-known, well-defined, clear, and (hopefully) simple patterns will lead to APIs that are predictable and easy to learn, which should lead to overall better
APIs. Now that we have a good grasp on APIs and what makes them good, let’s start thinking about higher-level patterns we can follow when designing them.
Summary
Interfaces are contracts that define how two systems should interact with one another.
APIs are special types of interfaces that define how two computer systems interact with one another, coming in many forms, such as downloadable libraries and web APIs.
Web APIs are special because they expose functionality over a network, hiding the specific implementation or computational requirements needed for that functionality.
Resource-oriented APIs are a way of designing APIs to reduce complexity by relying on a standard set of actions, called methods, across a limited set of things, called resources.
What makes APIs good
is a bit ambiguous, but generally good APIs are operational, expressive, simple, and predictable.
2 Introduction to API design patterns
This chapter covers
What an API design pattern is
Why API design patterns are important
The anatomy and structure of an API design pattern
Designing an API with and without design patterns
Now that we have a grasp of what APIs are and what makes them good,
we can explore how we might apply different patterns when building an API. We’ll start by exploring what API design patterns are, why they matter, and how they’ll be described in later chapters. Finally, we’ll look at an example API and see how using pre-built API design patterns can save lots of time and future headaches.
2.1 What are API design patterns?
Before we start exploring API design patterns we have to lay a bit of groundwork, starting with a simple question: what is a design pattern? If we note that software design refers to the structure or layout of some code written in order to solve a problem, then a software design pattern is what happens when a particular design can be applied over and over to lots of similar software problems, with only minor adjustments to suit different scenarios. This means that the pattern isn’t some pre-built library we use to solve an individual problem, but instead more of a blueprint for solving similarly structured problems.
If this seems too abstract, let’s firm it up and imagine that we want to put a shed in our backyard. There are a few different options to choose from, ranging from what we did a few hundred years ago to what we do today thanks to the magic of companies like Lowe’s and Home Depot. There are lots of options, but four common ones are as follows:
Buy a pre-built shed and put it in the backyard.
Buy a shed kit (blueprints and materials) and assemble it ourselves.
Buy a set of shed blueprints, modify the design as necessary, then build it ourselves.
Design and build the entire shed from scratch.
If we think of these in terms of their software equivalent, they would range from using a pre-built off-the-shelf software package all the way through writing an entirely custom system to solve our problem. In table 2.1 we see that these options get more and more difficult as we move through the list, but also add more and more flexibility from one option to the next. In other words, the least difficult has the least flexibility, and the most difficult has the most flexibility.
Table 2.1 Comparison of ways to build a shed with ways to build software
Software engineers tend to choose the build from scratch
option most of the time. Sometimes this is necessary, particularly in cases where the problems we’re solving are new. Other times this choice wins out in a cost-benefit analysis because our problem is just different enough to prevent us from relying on one of the easier options. And still other times we know of a library that happens to solve our problem exactly (or close enough), and we choose to rely on someone else having already solved the problem at hand. It turns out that choosing one of the in-between options (customizing existing software or building from a design document) is much less common but could probably be used more often with great results. And this is where design patterns fit in.
At a high level, design patterns are the build from blueprints
option applied to software. Just like blueprints for a shed come with the dimensions, locations of doors and windows, and the materials for the roof, design patterns come with some set of specifications and details for the code that we write. In software this often means specifying a high-level layout of the code as well as the nuances of relying on the layout to solve a particular design problem. However, it’s rare that a design pattern is made to be used entirely on its own. Most often, design patterns focus on specific components rather than entire systems. In other words, the blueprints focus on the shape of a single aspect (like the roof shape) or component (like a window design) rather than the entire shed. This might seem like a downside at first glance, but that’s only the case if the goal is exactly to build a shed. If you’re trying to build something sort of like a shed, but not quite a shed, then having blueprints for each individual component means you can mix and match lots of them together into exactly what you want to build, choosing roof shape A and window design B. This carries over into our discussion of design patterns, since each one tends to focus on a single component or problem type of your system, helping you build exactly what you want by assembling lots of pre-designed pieces.
For example, if you want to add debug logging to your system, you’ll likely want one and only one way to log messages. There are lots of ways you could do this (for example, using a single shared global variable), but there happens to be a design pattern aimed at solving this software problem. This pattern, described in the seminal work Design Patterns (Gamma et al., 1994), is called the singleton pattern, and it ensures that only a single instance of a class is created. This blueprint
calls for a class with a private constructor and a single static method called getInstance(), which always returns a single instance of the class (it handles creating that single instance if and only if it doesn’t exist yet). This pattern is not at all complete (after all, what good is it to have a singleton class that does nothing?); however, it’s a well-defined and well-tested pattern to follow when you need to solve this small compartmentalized problem of always having a single instance of a class.
Now that we know what software design patterns are generally, we have to ask the question: what are API design patterns? Using the definition of an API as described in chapter 1, an API design pattern is simply a software design pattern applied to an API rather than all software generally. This means that API design patterns, just like regular design patterns, are simply blueprints for ways of designing and structuring APIs. Since the focus is on the interface rather than the implementation, in most cases an API design pattern will focus on the interface exclusively, without necessarily building out the implementation. While most API design patterns will often remain silent on the underlying implementation of those interfaces, sometimes they dictate certain aspects of the API ’s behavior. For example, an API design pattern might specify that a certain RPC can be eventually consistent, meaning that the data returned from that RPC could be slightly stale (for example, it could be read from a cache rather than the authoritative storage system).
We’ll get into a more formal explanation of how we plan to document API patterns in a later section, but first let’s take a quick look at why we should care about API design patterns at all.
2.2 Why are API design patterns important?
As we already learned, API design patterns are useful in building APIs, just like blueprints are when building a shed: they act as pre-designed building blocks we can use in our projects. What we didn’t dig into is why we need these pre-designed blueprints in the first place. Aren’t we all smart enough to build good APIs? Don’t we know our business and technical problems best? While this is often the case, it turns out that some of the techniques we use to build really well-designed software don’t work as well when building APIs. More specifically, the iterative approach, advocated in particular by the agile development process, is difficult to apply when designing APIs. To see why, we have to look at two aspects of software systems. First, we have to explore the flexibility (or rigidity) of the various interfaces generally, and then we must understand what effect the audience of the interface has on our ability to make changes and iterate on the overall design. Let’s start by looking at flexibility.
As we saw in chapter 1, APIs are special kinds of interfaces that are made primarily so computing systems can interact with one another. While having programmatic access to a system is very valuable, it’s also much more fragile and brittle in that changes to the interface can easily cause failures for those using the interface. For example, changing the name of a field in an API would cause a failure for any code written by users before the name change occurred. From the perspective of the API server, the old code is asking for something using a name that no longer exists. This is a very different scenario from other kinds of interfaces, such as graphical user-interfaces (GUIs), which are used primarily by humans rather than computers and as a result are much more resilient to change. This means that even though a change might be frustrating or aesthetically displeasing, it typically won’t cause a catastrophic failure where we can no longer use the interface at all. For example, changing the color or location of a button on a web page might be ugly and inconvenient, but we can still figure out how to accomplish what we need to do with the interface.
We often refer to this aspect of an interface as its flexibility, saying that interfaces where users can easily accommodate changes are flexible and those where even small changes (like renaming fields) cause complete failures are rigid. This distinction is important because the ability to make lots and lots of changes is determined in large part by the flexibility of the interface. Most importantly we can see that rigid interfaces make it much more difficult for us to iterate toward a great design like we would in other software projects. This means that we often end up stuck with all design decisions, both good and bad. This might lead you to think that the rigidity of APIs implies we’ll never be able to use an iterative development process, but this is not always the case thanks to another important aspect of interfaces: visibility.
Generally, we can put most interfaces into two different categories: those that your users can see and interact with (in software usually called the frontend) and those that they can’t (usually called the backend). For example, we can easily see the graphical user interface for Facebook when we open a browser; however, we don’t have the ability to see how Facebook stores our social graph and other data. To use more formal terms for this aspect of visibility, we can say that the frontend (the part that all users see and interact with) is usually considered public and the backend (only visible to a smaller internal group) is considered private. This distinction is important because it partly determines our ability to make changes to different kinds of interfaces, particularly rigid ones like APIs.
If we make a change to a public interface, the whole world will see it and may be affected by it. Since the audience is so large, carelessly making changes could result in angry or frustrated users. While this certainly applies to rigid interfaces like APIs, it also applies to flexible interfaces just the same. For example, in the early days of Facebook most major functional or design changes caused outrage among college students for a few weeks. But what if the interface isn’t public? Is it a big deal to make changes to backend interfaces that are only seen by members of some private internal group of people? In this scenario the number of users affected by a change is much smaller, possibly even limited to people on the same team or in the same office, so it seems we have gained back a bit more freedom to make changes. This is great news because it means we should be able to iterate quickly toward an ideal design, applying agile principles along the way.
So why are APIs special? It turns out that when we design many APIs (which are rigid by definition) and share them with the world, we really have a worst-case scenario for both aspects. This means that making changes is much more difficult than any other combination of these two properties (as summarized in table 2.2).
Table 2.2 Difficulty of changing various interfaces
Put simply, this worst of both worlds
scenario (both rigid and difficult to change) makes reusable and proven design patterns even more important for building APIs than other types of software. While code is often private and out of sight in most software projects, design decisions in an API are front and center, shown to all of the users of the service. Since this seriously limits our ability to make incremental improvements on our designs, relying on existing patterns that have survived the tests of time are very valuable in getting things right the first time rather than just eventually as in most software.
Now that we’ve explored some of the reasons these design patterns are important, let’s get into an API design pattern by dissecting it and exploring its various components.
2.3 Anatomy of an API design pattern
Like most pieces in software design, API design patterns are made up of several different components, each one responsible for a different aspect of consuming the pattern itself. Obviously the primary component focuses on how the pattern itself works, but there are other components targeted at the less technical aspects of consuming a design pattern. These are things like figuring out that a pattern exists for a given set of problems, understanding whether the pattern is a good fit for the problem you’re dealing with, and understanding why the pattern does things in one way rather than using a (possibly simpler) alternative.
Since this anatomy lesson could get a bit complicated, let’s imagine that we’re building a service that stores data and that the customers of that service want an API where they can get their data out of the service. We’ll rely on this example scenario to guide our discussion through each of the pattern components that we’ll explore next, starting with the beginning: the name.
2.3.1 Name and synopsis
Each design pattern in the catalog has a name, given to uniquely identify the pattern in the catalog. The name will be descriptive enough to convey what the pattern is doing, but not so long-winded that it’s not easy to shout across a noisy room. For example, when describing a pattern that solves our example scenario of exporting data, we could call it Import, export, back-up, restore, snapshot, and rollback pattern,
but it’s probably better named as Input/Output pattern
or IO pattern
for short.
While the name itself is usually enough to understand and identify the pattern, it’s sometimes not quite verbose enough to sufficiently explain the problem that the pattern addresses. To ensure there is a short and simple introduction to the pattern itself, there will also be a short summary of the pattern following the name, which will have a brief description of the problem it is aiming to solve. For example, we might say that the input/output pattern offers a structured way of moving data to or from a variety of different storage sources and destinations.
In short, the overall goal of this section is to make it easy to quickly identify whether any particular pattern is worth further investigation as a potential fit for solving a given problem.
2.3.2 Motivation
Since the goal of an API design pattern is to provide a solution for a category of problems, a good place to start is a definition of the problem space the pattern aims to cover. This section aims to explain the fundamental problem so that it’s easy to understand why we need a pattern for it in the first place. This means we first need a detailed problem statement, which often comes in the form of a user-focused objective. In the case of our data export example, we might have a scenario where a user wants to export some data from the service into another external storage system.
After that, we must dig a bit deeper into the details of what users want to accomplish. For example, we might find that users need to export their data to a variety of storage systems, not just Amazon’s S3. They also may need to apply further constraints on how the data is exported, such as whether it’s compressed or encrypted before transmission. These requirements will have a direct impact on the design pattern itself, so it’s important that we articulate these details of the problem we’re addressing with this particular pattern.
Next, once we understand the user objectives more fully, we need to explore the edge cases that are likely to arise in the normal course of actual implementation. For example, we should understand how the system should behave when the data is too large (and how large is too large, since those words often mean different numbers to different people). We also must explore how the system should react in failure scenarios. For example, when an export job fails we should describe whether it should be retried. These unusual scenarios are likely to be much more common than we typically expect, and even though we might not have to decide how to address each scenario right away, it’s critical that the pattern take note of these blanks so that they can eventually be filled in by an implementation.
2.3.3 Overview
Now we’re getting closer to the fun part: explaining what the design pattern recommends as a solution to the problem space. At this point, we’re no longer focused on defining the problem, but on offering a high-level description of the solution. This means that we get to start exploring the tactics we’ll employ to address the problem and the methods we’ll use to do so. For example, in our exporting data scenario, this section would outline the various components and their responsibilities, such as a component for describing the details of what data to export, another for describing the storage system that acts as a destination for the exported data, and still another for describing encryption and compression settings applied before sending the data to that destination.
In many cases the problem definition and list of solution requirements will dictate a general outline of a solution. In those cases, the goal of the overview is to explicitly articulate this outline rather than leaving it to be inferred from the problem description, regardless of how obvious a solution may seem. For example, if we’re defining a pattern for searching through a list of resources, it seems pretty obvious to have a query parameter; however, other aspects (such as the format of that parameter or the consistency guarantees of the search) might not be so obvious and merit further discussion. After all, even obvious solutions may have subtle implications that are worth addressing, and, as they say, the devil is often in the details.
Other times, while the problem is well-defined, there may not be a single obvious solution, but instead several different options that may each have their own trade-offs. For example, there are many different ways to model many-to-many relationships in an API, each with its different benefits and drawbacks; however, it’s important that an API choose one option and apply it consistently. In cases like this, the overview will discuss each of the different options and the strategy employed by the recommended pattern. This section might contain a brief discussion of the benefits and drawbacks of the other possible options mentioned, but the bulk of that discussion will be left for the trade-offs section at the end of the pattern description.
2.3.4 Implementation
We’ve gotten to the most important piece of every design pattern: how we go about implementing it. At this point, we should thoroughly understand the problem space that we’re trying to address and have an idea of the high-level tactics and strategy we’ll be employing to solve it. The most important piece of this section will be interface definitions defined as code, which explain what an API using this pattern to solve a problem would look like. The API definitions will focus on the structure of resources and the various specific ways to interact with those resources. This will include a variety of things such as the fields present on resources or requests, the format of the data that could go into those fields (e.g., Base64 encoded strings), as well as how the resources relate to one another (e.g., hierarchical relationships).
In many cases, the API surface and field definitions themselves may not be sufficient to explain how the API actually works. In other words, while the structure and list of fields may seem clear, the behavior of those structures and interaction between different fields may be much more complex rather than simple and obvious. In those cases, we’ll need a more detailed discussion of these non-obvious aspects of the design. For example, when exporting data we may specify a way to compress it on the way to the storage service using a string field to specify the compression algorithm. In this situation, the pattern might discuss the various possible values of this field (it might use the same format used by the Accept-Encoding HTTP header), what to do when an invalid option is supplied (it might return an error), and what it means when a request leaves the field blank (it might default to gzip compression).
Finally, this section will include an example API definition, with comments explaining what an API that correctly implements this pattern should look like. This will be defined in code, with comments explaining the behaviors of the various fields, and will rely on a specific example of a scenario illustrating the problem that’s addressed by the pattern. This section will almost certainly be the longest and have the most detail.
2.3.5 Trade-offs
At this point we understand what a design pattern gives us, but we’ve yet to discuss what it takes away, which is actually pretty important. Put bluntly, there may simply be things that are not possible if the design pattern is implemented as designed. In these cases it’s very important to understand what sacrifices are necessary in order to achieve the benefits that come from relying on a design pattern. The possibilities here are quite varied, ranging from functional limitations (e.g., it’s impossible to export data directly as a download to the user in a web browser) to increased complexity (e.g., it’s much more typing to describe where you want to send your data), and even to more technical aspects like data consistency (e.g., you can see data that might be a bit stale, but you can’t know for sure), so the discussion can range from simple explanations to detailed exploration of the subtle limitations when relying on a particular design pattern.
Additionally, while a given design pattern may often fit the problem space perfectly, there will certainly be scenarios where it is a close enough fit but not quite perfect. In these cases it’s important to understand what consequences will arise by relying on a design pattern that is in this unique spot: not the wrong pattern, but not quite a perfect pattern either. This section will discuss the consequences of slight misalignments like this.
Now that we’ve gotten a better grasp on how API design patterns will be structured and explained, let’s switch gears and look at the difference these patterns can make when building a supposedly simple API.
2.4 Case study: Twapi, a Twitter-like API
If you’re unfamiliar with Twitter, think of it like a place where you can share short messages with others—that’s it. It’s a little scary to think that an entire business is built on everyone creating tiny messages, but apparently that’s enough to merit a multi-billion dollar technology company. What’s not mentioned here is that even with an extremely simple concept, there happens to be quite a lot of complexity hiding underneath the surface. To better understand this, let’s begin by exploring what an API for Twitter might look like, which we’ll call Twapi.
2.4.1 Overview
With Twapi, our primary responsibility is to allow people to post new messages and view messages posted by other people. On the surface this looks pretty simple, but as you might guess, there are a few hidden pitfalls for us to be aware of. Let’s start by assuming that we have a simple API call to create a Twapi message. After that, we’ll look at two additional actions this API might need: listing lots of messages and exporting all messages to a different storage system.
Before we get moving, there are two important things to consider. First, this will be an example API only. That means that the focus will remain on the way in which we define the interface and not on how the implementation actually works. In programming terms, it’s a bit like saying we’ll only talk about the function definition and leave the body of the function to