A URL identifies a resource; everything describing how you want to look at it — order, page, which fields — belongs in the query string, because two clients asking for the same books in a different order are still asking about the same books. In https://api.example.com/api/v1/books/bk_012/reviews?sort=-year each segment has one job: the origin carries no API names, /api/v1 is a version chosen once, books is a plural lowercase collection, bk_012 is an opaque item id, reviews is a sub-collection, and the query string is a view of the set rather than part of its identity.
| Instead of | Write | Because |
|---|---|---|
| /getBooks, /books/search | /books?q=dune | The verb is the method |
| /book/12 | /books/bk_012 | Plural, with an opaque id |
| /books/12/delete | DELETE /books/bk_012 | GET must stay safe |
| /authors/a_7/books/bk_012 | /books/bk_012 | One resource, one URL |
| /books.json | Accept: application/json | Format is negotiated |
Nesting is where most APIs go wrong. /books/bk_012/reviews is right, because a review has no meaning without its book; /authors/a_7/books/bk_012 is wrong, because the book has its own identity and now has two URLs, two cache entries and a 404 puzzle when someone reassigns the author. Nest one level for dependent children, then stop. Sequential integer ids leak information too, while opaque ids cost nothing and are what MongoDB 1,815 gives you anyway (MongoDB).