Design Your APIs for Humans, Not Just Other Systems

Why the Developer Experience Matters

Some Systems Live-or-Die Based on Who’s Calling Them

Applying the Human Centered Design Process to API Design

  1. Empathize — First off, you should truly understand the sorts of systems that are likely to interact with yours, and the subsequent people creating & maintaining said systems. This may feel a bit too “soft” for some, but remember that developers are people too, with a lot to think about and usually not enough time to think through it all. So try creating “user profiles” of systems that will (or are likely to) interact with your APIs, and if possible reach out to the people who build said systems. Describe the purpose of your system (and certain aspects of the data model & functionality if necessary), then work with them to understand how your system could be leveraged by their system to outsource non-novel functionality, continue a business flow or user journey, or simplify development tasks for them. Even better, if they’re already interacting with a similar system or have written code themselves to fulfill a purpose that your system can provide, see what their biggest pain points are & whether your system could solve them without sacrificing aspects of their current state that they like.
  2. Define— Next step would be to define the problems that you’ll look to solve through your system’s APIs. These can be expressed through high-level user stories that feed off of the opportunity areas identified during the Empathize stage, alongside other potential CRUD (Create, Read, Update, Delete) needs concerning resources in your system. At this point you’re not looking to define specific APIs, but instead describe at a high level the interactions that consumers should be able to have with your system. For example: Instead of saying “consumers should have access to a Get User API and a Get Orders API”, I recommend defining the problem statement as “consumers need to be able to access users’ information, including their order history”. This leaves the door open to later design discussions, like whether the API(s) that fit this need should aggregate certain backend information vs. require separate calls, and whether they should allow arrays to be passed in (fetching orders for multiple users vs. one user at a time). An interesting question to consider is how many potential needs you should account for, on top of identified needs. There are many instances in API design where the team thinks: “Someone at some point may need to perform x operation on y resource(s), but no one’s asked for it yet”. Opinions on this topic range from allowing all CRUD operations on all resources by default (so long as they don’t pose security or data integrity concerns) to only building APIs that consumers (existing or potential) explicitly ask for. My take is that it’s a judgement call, dependent upon the odds of the potential need becoming actualized vs. the time it would probably take to deliver, expressed in both cycle time (time between the consumer asking for it & it being delivered?) & development time, alongside any security & data integrity concerns.
  3. Ideate — For each of the problems identified in the Define stage, begin identifying a variety of potential solutions via APIs, and how each of those solutions would shape the system’s broader API landscape. The ideal state is usually a 1–1 mapping between resources and APIs (ex. a User API to handle CRUD operations on User resources, an Order API to do the same for Order resources, etc.). However, during the problem-driven ideation process you may find that a lot of 1–1 mapped solutions would require the consumers to make chained calls to various APIs in order to meet their identified needs. This can feel unintuitive to consumers unfamiliar with your data model, and can lead to performance issues. In that case aggregation (fetching or operating on multiple resources from one call) may become a commonly-used API design pattern in your system to reduce complexity & calling-load for consumers, even if it may yield increased brittleness, system load, and unexpected side-effects. By first identifying various solutions to each problem, then looking at the whole palette of options, your team can make more intelligent decisions about the system’s current & future API design philosophy, accounting for it when selecting which designs to use.
  4. Prototype & Test — Prototyping can have two steps. First would be to define & run your team’s API designs past the consumers identified in step one to get feedback on their clarity, intuitiveness, and usefulness. Then once designs are tweaked (taking said feedback into account), it may make sense for your team to mock out these API definitions, allowing soon-to-consume systems to call said mocks and receive example data back while building their own integrations to your system. This goes beyond eyeballing & rubber-stamping, to allowing developers from other systems to experience your APIs and provide more accurate feedback on their clarity, intuitiveness, & usefulness. Once your team & all participating consumers feel good (or at least good enough) about the designs, feel free to start putting some actual logic behind those APIs.
  5. Iterate — Iteration may be the best thing to come out of agile philosophy in my opinion, and it’s because humans are notoriously bad at predicting the future. Many bad products & designs had at least one person behind the scenes who thought they would work at time-of-creation, but turned out to be wrong. And even if they nailed that design initially, operating environments shift, and what seemed awesome at release can soon become become clunky, outdated, & effectively useless. Systems change over time as functionality is iteratively built & updated. As a result API designs that worked great at-creation may no longer enable consumers’ needs, or may no longer reflect the system’s underlying design. That’s why it’s important to reconsider the system’s existing APIs regularly, even running through a miniature version of the HCD process described above to ensure these APIs remain clear, intuitive, & useful.

Other API Design Considerations

  • Arrays on Input? — For any API that’s used to perform CRUD operations on resources, the odds are strong that there’s a use case for consumers to provide an array of stuff to create, read, update, or delete all at once, instead of passing it one-at-a-time. By allowing an array of ids, new resource definitions, etc. in your API’s inputs, you can reduce the chattiness of intersystem communication, generally improving performance. On the flip side accepting arrays mean a lot more for you to think about. What should happen if the requested operation fails on one resource, but works on the rest? How many resources or ids should other systems be able to send you at one time? How much complexity would handling multiple resources or ids add to your system’s underlying logic? All pros & cons to weigh when deciding whether to handle parameters in bulk.
  • Have a Method to Using HTTP Methods — HTTP methods aren’t hard to learn, and there’s a sea of websites that describe what each one’s meant for, yet far too many APIs expect a method that differs from the actual operation they perform. Furthermore, some teams will define an entirely new API to meet a need, when they could just add a new method to an existing endpoint. The fact that HTTP methods are standardized makes them a powerful tool, since you don’t need to define and teach consumers about new verbs & their meanings for them to interact with your APIs. By specifying that an endpoint accepts certain standard methods (ex. the User API supports GET, POST, and PUT) it’s made immediately clear to new consumers what they can (and can’t) do.
  • Kowalski, Status Report! — On the other end of API calls, consider the range of results that may come from your API being called, and the status code said API should return for each. Everything from expected results (most likely 200 codes) to client-side failures like missing or incorrect parameters (400 codes) to server side failures (500 codes). Don’t be afraid to return codes that don’t end in two zeros, if they better represent the actual state resulting from the API call.
  • Paint a Picture with Your Response Messages — In addition to accurate, meaningful status codes, look to provide more context around what happened (or is happening) through the response message given back to the caller. Especially in 400-style scenarios where the client called the API incorrectly. Why exactly did their call fail? What parameter(s) is/are causing the issue? Giving meaningful feedback to the consumer will save significant headaches, both on their side and yours (since you’ll be the first person they call at 2am when all calls to your service are failing).

--

--

Get the Medium app

A button that says 'Download on the App Store', and if clicked it will lead you to the iOS App store
A button that says 'Get it on, Google Play', and if clicked it will lead you to the Google Play store