5 URLs
In the last chapter, we talked about some of the basics behind HTTP, but we didn’t really talk about how to go the process of designing an API. So we’re going to do that now. We’re going to design the URL endpoints for an imaginary blog. Again, if you’re super extra 100% with sprinkles on top sure that you have this stuff down, go ahead to the next chapter, where I promise to start being entertainingly upset over bad API design. If you’re in doubt, continue on with this chapter. It’s interesting, I promise.
The Conceptual Structure
In the last chapter, we talked about the structure of URL endpoints, and we decided that they can be structured as a hierarchy of resources.
Our hypothetical blog has several resources we need to provide access to:
- Articles, which are the content of the blog.
- Authors, who write the content.
- Comments, which are responses to the content of the blog.
These resources have a hierarchy to them that we’ll explore as we construct the URLs.
Let’s start with Articles. The URLs for Articles can be pretty simple—as they should be, as Articles are going to be the content the audience wants to access most frequently.
For example, retrieving a list of the most recent Articles could look like this:
1 GET /articles
Retrieving a single Article (maybe the Article with an ID of 4) would then be a simple extension of that:
1 GET /articles/4
The idea is that you want the resource identified by 4 that is a member of the articles resource—remember, resources can contain other resources.
The important thing to remember is that URL endpoints should only contain nouns. That means that verbs like create or get should never be found in URLs—that’s what the HTTP verbs are for, as we discussed in the last chapter.
Filter or Endpoint?
URLs have a “query string” that allows you specify “query parameters”. In /articles?published=true&shared=false, published and shared are both query parameters. published has a value of true, and shared has a value of false. Note that parameters and values can be any string, though you should make sure to URL encode them.
Semantically, query parameters and their values are meant to filter the resources returned. In the sample above, only resources that have a published property set to true and a shared property set to false should be returned.
This raises a conundrum, however. It’s best illustrated by trying to construct our Comments endpoint, so let’s do that now. The simple way to do that is to just create an Comments endpoint, like we did for Articles:
1 GET /comments
This is fine. There’s nothing wrong with this. But this could be a bit easier. Because how does your audience typically want to use this information? They want to retrieve Comments on a specific Article. Assuming the Comments have an article_id property containing the ID of the Article the Comment belongs to, you can always use a query parameter to filter the Comments:
1 GET /comments?article_id=4
Semantically, that means “show me all the Comments whose article_id property is set to 4”, which will retrieve the list of Comments you want, so it’s a valid API design decision.
But that logic isn’t how your user approaches the problem. They don’t want a list of Comments that have a property set to a value, they want a list of Comments that belong to an Article. That suggests a hierarchy:
1 GET /articles/4/comments
Semantically, that says “show me a list of Comments that belong to the Article with ID 4.” See how that aligns better with the user’s approach to the request? That affordance will make your API easier to reason about and, therefore, use.
We need to be careful, though, because look at the functionality that was lost to that affordance: we can no longer retrieve a list of Comments, regardless of the Article they belong to. If we want to say “show me all the Comments”, we can’t. We have to go through each Article and retrieve its Comments manually.
Which may be fine. That may be ideal, based on your backend and the capabilities you want to offer. It’s just important to be mindful of the restrictions your decisions create and to offer affordances without unnecessarily restricting use cases. A balance needs to be struck between versatility and ease of use.
Should you want to offer both, the affordance and the versatility of being able to list all the Comments, there’s no crime in including both:
1 GET /comments?article_id=4
2 GET /articles/4/comments
Consider it a helper endpoint that calls out the common use-case. While it’s technically undesirable to have multiple endpoints representing the same resource, the benefit of making typical use cases obvious can outweigh the technical purity.
As always, API design, like all design, is contextual. You need to examine your API and its needs, and try to meet your users’ expectations with as little friction as possible.
Pseudo-Resources
Some APIs have a practice of offering, for example, an /authors/me endpoint that returns the authenticating Author. I like to call these endpoints “pseudo-resources”, because they don’t point to a single resource, they just proxy to a contextual resource.
This is problematic. First, because URLs are supposed to refer to the same conceptual resource. If I request /authors/1, I should get the same thing you get.
But that’s a question of technical purity, and that’s undesirable in some situations. For example, if I have admin access to the blog, /articles may return unpublished posts for me, but not for you. So, technically, the resource would be different, unless you contort the definition of that resource in unintuitive ways. And yet, that’s the intuitive response that your user expects, so that should be how your API works. Technical purity is a great way to decide between two equivalent implementations, but it should always take a backseat to the experience of your user.
The more important thing to remember is that it may sometimes be desirable to store more than one authenticating user’s response for the same resource. Consider a situation like Twitter, where you may have multiple accounts. Returning a different resource for /authors/me breaks the caching expectations and adds a layer of complexity.
The worst part is that it doesn’t need to be there. Your users know who they are. They logged in. They don’t need you to remind them. You don’t need an /articles/newest endpoint; just use /articles, sort by the publish date, and limit the request to 1 result.
That is, unless the entire point of your application is to somehow show only the latest Article. In that case, you absolutely should have an /articles/newest endpoint.
Pagination
I mentioned it in that last section, so we may as well cover it now: how do we tell the server how many resources we want back, or which range of resources, or how they should be ordered?
There are a few approaches to this. The way most APIs approach it is to include that information in the query parameters. E.g., /articles?sort_by=date_published&order=desc would tell the server it wants the Articles sorted by the date they were published, with the most recent dates first.
And that’s fine. There is a technical argument to be made there: the query string is meant to filter a resource, not order them. These APIs (again, a majority of the ones I’ve used) appropriate the query string as a place to include meta information.
Why? We already have headers for that purpose, as we discussed in the previous chapter. If you want to specify the bytes you get in response, you use the Range header. Why wouldn’t you use a header to specify the range of resources you want or how you want them returned?
The answer is that, once again, it’s a trade-off. While returning that information in the headers is going to make your query string cleaner and maintain technical purity, it means it’s harder to debug and explore your API in the browser. I, personally, don’t frequently debug APIs using the browser, but I’m a minority in that respect. So a lot of APIs are designed around web browser limitations, to make it easier for developers to work with them.
This is great. I love that this is happening. It means that API designers are taking into consideration the way that people are using their APIs and designing to make that easier.
Personally, I prefer to have the pagination information in the headers, because it’s technically purer, presents a cleaner query string, and has no drawbacks for me. But that’s a preference; my life is not made harder by having the pagination information in the query string, so it’s really just a tradeoff. Does the aesthetic advantage outweigh the loss of debugging with the browser?
Wrapping Up
In this chapter, we talked about:
- How to approach API design.
- Query parameters and values, the way resources are filtered.
- URL endpoints, the way resources are accessed.
- Pseudo elements and their problems.
- Pagination and browser debugging.
If there’s a key takeaway, it’s this:
Good API design is not a matter of following principles, it’s a matter of fulfilling your users’ expectations while trying to maintain technical and semantic purity.
User expectations trump technical and semantic purity, but tossing technical and semantic purity out the window will yield an API that’s hard to reason about and is very constrained. API design is the process of balancing these concerns.
With that in mind, let’s talk about some bad API design decisions. What is a bad API design decision? It’s not just a matter of opinion or aesthetic; those are API preferences, like my preference for pagination in the header.
A bad API decision is a decision that sacrifices user experience without gaining anything or where the gains are so incredibly outweighed by the problems exposed, no designer would ever consciously make that choice.
In short, a bad API decision is simply the lack of a conscious decision.
Let’s take a look at some of those in the next chapter, which is going to cover requests and responses.