Basis Lab

Answers · The network and the web

What's wrong with naming an API endpoint after an action like /getUser?

In shortNaming an endpoint after an action bakes one operation into its address, so every new operation needs a new endpoint and the thing being acted on never gets a stable name.

Naming an endpoint after an action bakes one operation into the address, so every new operation needs another address and the thing being acted on never gets a stable name. The interface grows by adding top-level entries, not by adding operations to a thing callers already know.

The same capability counted two ways

Take one thing, a user, and the five operations callers actually want: read one, list, create, update and delete. Action-shaped addresses give five top-level entries: /getUser, /listUsers, /createUser, /updateUser, /deleteUser. Thing-shaped addresses give two: /users for the collection and /users/{id} for one member. Five minus two is three fewer top-level names before any nesting, and the saved names are not the point. The point is that /users/{id} already has a place for read, update and delete, while /getUser has a place for read and nothing else. A caller who has learned /users/{id} can guess where /users/{id}/orders lives. A caller who has learned /getUser has learned one entry and can guess nothing. At a larger scale the same count held: nine action entries against four thing families is five extra top-level names, because nine minus four is five.

FIG 1
what the caller wantsaction-shaped entrything-shaped address
read one user/getUser?id=42/users/42
list users/listUsers/users
create a user/createUser/users
update a user/updateUser?id=42/users/42
delete a user/deleteUser?id=42/users/42

What the verb hides

The natural guess is that /getUser is bad because a verb in a URL is untidy, or because some style guide says so. That complaint is too weak to change anybody's mind, and it misses the actual cost. The verb is not the problem by itself; the problem is that the verb is the only place the operation can live, so the thing has no address of its own. With GET /users/42 the method says read and the path says which user. With GET /getUser?id=42 the path says read, the query says which user, and GET says read again. The identifier sits outside the path, so every action invents its own way of passing it: id here, userId there, user_id in the next entry. That is three conventions for one thing, and a caller has to learn all three.

Each action entry also brings its own shape, parameters and failure modes. /getUser returns one user or a not-found error. /listUsers returns an array, possibly an empty one, and has paging parameters that /getUser does not. /createUser takes a body, returns a created user and has validation failures. /deleteUser returns nothing, or a conflict if the user cannot be removed. A caller cannot learn one shape and reuse it across the set, because there is no set. There are five unrelated endpoints that happen to mention the same noun in their names.

The fix is not to ban verbs. It is to put the verb in the operation position and the noun in the address. If the operation is read one user, the address names the user and the operation is read. If the operation is cancel, the address names the order and the operation is cancel. The operation list is short and fixed; the thing list is where growth belongs, and it grows only when a genuinely new thing appears.

The most useful signal is a verb that will not fit any noun. If somebody asks for /approveOrder and there is nowhere to put the approver, the time, the note or the withdrawal, the verb is a thing in disguise. Approving produces an approval: it has an identifier, a time, an author, a reason and a life of its own. Name the approval under the order and the awkward verb becomes an ordinary create, list or withdraw on a thing. That reframing does more than tidy the path. The approval can be looked up later, its author has a column, and withdrawing it is an operation on a thing rather than a second verb bolted onto the order.

The cost of getting this wrong is not paid on the first day. It is paid when the fifth operation arrives and there is no fifth place to put it, so a sixth top-level entry appears with its own parameters and its own failures. The list grows by one every time anybody wants anything. The thing list grows only when a genuinely new thing appears, and every operation on a known thing already has an obvious shape.

When an action is honest

This stops being true when the operation genuinely produces nothing that can be referred to later. A currency conversion, a checksum, or a search across many things that returns no persistent result: there is no identifier, no time, no author, no thing to name, and an action-shaped endpoint can be the honest shape. The test is not whether the word is a verb. The test is whether the result has an identity. If it does, the missing noun is the thing the verb was going to create. If it does not, there is nothing to name and the action is the interface.

Worth keeping

  • An endpoint named after an action makes every new operation a new top-level name.
  • A thing-shaped address lets the operations follow from the thing, so callers learn one shape and reuse it.
  • A verb that will not fit any noun is evidence that the noun is missing, and the missing noun is nearly always the thing the verb was going to create.

Where this goes next

This question is one step of Designing an Interface to Call, a course that works through its subject from the beginning. The page that teaches this part of it is Naming the Things, and it reads the whole way through whether or not you have an account.

What an account changes is the practice: problems standing under every idea, marked with a reason rather than a tick, in a copy of the course that is yours.

Read that pageThe whole course

Other questions people ask about this

  1. What does it mean for an API method to be safe?A safe method is one that changes nothing on the server when it is called, so a caller may repeat it freely and a cache or a crawler may use it without anything being altered.
  2. How do I design an error response that a client can actually handle?Give every failure a fixed code a program can branch on and say plainly whether trying again could help, then put the human explanation last.
  3. What's the difference between safe and idempotent in HTTP methods?Safe means the call changes nothing; idempotent means repeating the call leaves the same result, even if the first call changed something.
  4. Why do I need an idempotency key if my operation is already idempotent?You need the key because repeating an operation safely still leaves the caller unsure what happened, and the key returns the first answer.
  5. Where should a session ID be stored so JavaScript can't read it?Store it in an HttpOnly cookie set by the server; that is the only browser storage JavaScript running on the page cannot read.
  6. Does signing out actually invalidate my token on the server?No. Signing out clears your browser's copy of the token, but the server keeps accepting that token until it expires.

Read alongside