There’s a lot of chatter these days about the problems with REST, as well as a whole host of proposed solutions. GraphQL. Falcor. Om.next. These are all interesting tools with their own sets of tradeoffs, but not everyone has the luxury of starting a greenfield project or throwing out REST entirely. What about the thousands of existing REST APIs being actively maintained? What about the years of tooling and experience with building REST APIs? REST isn’t going anywhere right now, but don’t fret. You can still architect RESTful APIs in ways that mitigate its problems and even make migration to something else easier.
This may seem obvious, but it can get confusing when you’re used to thinking in terms of process. Users sign in, they search, they sign out. While it’s important to consider these flows when building an application, I recommend approaching RESTful API design as a data modeling problem first. What resources (nouns) does your application expose?
For example, instead of sign-in and sign-out endpoint, why not use a
session resource? Signing in could create a session token, and signing out could delete it. Keep in mind too that "data modeling" is not the same as having a database schema; a session token may not ever be stored in a database. Thinking in terms of data first helps avoid building an RPC API masquerading as REST. Not to say that RPC is always bad, but its use should be deliberate, and your endpoints should be named (with verbs) accordingly.
Minimize side effects.
Nouns are great, but are they enough? What about, for example, user sign up? Signing up usually means more than just creating a user account. At the very least, it’s usually necessary to send a confirmation email. One naive approach would be adding logic to the user creation endpoint to send an email. However, there are some problems. What happens if a temporary failure is encountered and you need to retry? More importantly, you're complicating the user creation endpoint and consequently the user resource. You’re back to RPC. It becomes harder to test in isolation, and harder to reason about, especially as your API grows.
An alternate approach would be to just create a user resource, with a flag indicating whether or not the user has confirmed their email. Now a background worker can periodically poll for unconfirmed users and send out emails. It’s easier to retry and track the number of attempts, and it’s also easier to test each piece in isolation. Most importantly, the user creation endpoint does only what it should be doing: creating users.
Minimizing side effects can be thought of as a specific example of a more general principal: maintaining consistency. When considering consistency, I find it helpful to remember the purpose of the API: to expose a data model for your application. With that in mind, I hope it’s easier to see why you should minimize side effects beyond the mutation of your data model: additional side effects turn your endpoints into “special cases” with their own rules and behavior.
As if that weren’t bad enough, it’s also possible for special cases to creep in for endpoints that just read data. Often endpoints are added in response to feature requests, and it can be very tempting to return all the data you’ll need for a particular feature. For example, you’d like to show the five most recent purchases for a user after they sign in, so you have the user endpoint also return a purchase history. It may sound look like an optimization, but it’s important to remember the cost. These kinds of special case micro-optimizations often complicate larger cross-cutting optimizations.
I like to think of my REST API as a connected graph. All read requests return a set of nodes of a particular logical type, and all mutation requests modify one logical entity. A read request like
/user/1/hat would return all the hats for user 1, and
/hat would return all hats. A mutation request for
/user/1/hat/2 would mutate hat 2 for user 1, and would be equivalent to mutating
/hat/2 . If all endpoints in the REST API adhere to these guidelines, the interface itself becomes an implementation detail. Swapping out REST for GraphQL becomes a lot less scary.
Now go REST. You look like you need it!