This is a cross-post of a guest post I did for APIUX. Click here for the original..
URL design discussions for RESTful web services often degrade into debates over pluralization and parameter names. There are a couple of principles I like to use to keep things simple.
1) Using your API should feel like using a filesystem
- Endpoints used to create, list, and search for entities should look like directories, e.g.
/users
- Use a plural noun so it feels like a directory of users, not a user controller
- Endpoints used to read, update, and delete individual entities should look like files, e.g.
/users/charlie
2) All calls to a given endpoint should return the same type
- Either apples, or oranges, or a list of oranges, don’t mix them up
- File-looking endpoints should return individual entities
- Directory-looking endpoints should return lists of entities
We may be bikeshedding here but I think your API will be more intuitive to newcomers if you model it this way.
- Consistent response type per endpoint simplifies deserialization for clients, no switching needed
- Once you agree on a contract it’s easy to mock up with static files on a server
- Clients can start working with your mockup before your code is finished
Example Operations on Endpoints that look like Files
Read
GET /users/charlie
200 OK
{username: charlie, state: VA}
Update
PUT|PATCH /users/charlie
[password=1111]
Delete
DELETE /users/charlie
Example Operations on Endpoints that Look Like Directories
List
GET /users?start=40&count=20
200 OK
[{username: charlie, state: VA}, ...]
Search
GET /users?q=cha
Create
POST /users
{username=charlie&password=1234}
201 CREATED
Location: /users/charlie
Directory endpoints are supposed to return lists (or nothing). So send back a pointer to the new user record, rather than the user data itself.
POST /users/charlie
Also ok IFF you allow clients to generate entity ids
Serious Bikeshedding
Here’s other stuff I like:
Use a filename extension instead of the Accept header to express the response format:
http://api.example.com/users/charlie.json
- Not pure REST but it makes your API easier for devs to poke with curl and the browser. Easier to use means better adoption!
- Friendlier on stupid caches that improperly handle headers since the endpoint always sends back the same bytes (within the ttl)
- You can use .html (or no extension) to request the web representation and serve your API and web views with the same controller logic (though this can turn into a rabbit hole)
Another vote against the Accept header: version your endpoints in the URL, at web-application level:
http://q.addthis.com/feeds/1.0/trending.json?pubid=atblog
- feeds.war is the webapp and 1.0 is the version of the released artifact (which is 1.0.3 internally)
- Version numbers should correspond to deployable artifacts so they’re easier to manage
- You’ll need to keep old versions online in real-world use cases
Dropwizard has a nice model for organizing your projects.
Some javascript/flash API consumers will pressure you to add a suppress_response_codes parameter and always send them a 200. You’ll hate it but will end up giving in (just wait).
- You’ll need to define a standard error response envelope that they can switch on to check for errors and extract the reason. It will get messy for them and they will lose a lot of the benefits of this design.
- Anyone have better ideas here?