Awesome HTTP Semantics You Should Know

Awesome HTTP Semantics You Should Know
Photo by Towfiqu barbhuiya / Unsplash

Software engineers love to debate semantics in their code. A discussion on what would be the best name for a function or variable can easily lead to dozens of comments in a code review. And yet, for some odd reason, many engineers are not as passionate about HTTP semantics such as the different HTTP verbs and status codes.

This is a small piece about some awesome HTTP semantics that I think are overlooked or forgotten in most projects. Whether or not they actually bring any major value is debatable. Some suggestions here might feel like an "artisanal" thing so use your own judgment to see which of these are worth a refactor. But once you learn the semantics correctly, they won’t require much extra worth to implement correctly in a new endpoint or project.

Don’t Just Use POST

There are so many examples of incorrect usage of the POST method. Here are some examples:

  1. Updating or creating a resource in an idempotent way should be done via PUT method. With PUT methods we promise a certain behaviour: the target resource will be created or replaced with the state defined in the request message content. There will be no other side effects and executing the request multiple times will always lead to the same result. (But it could lead to a different response, more on that later!)
  2. Deleting resources should be done via the aptly named DELETE method.
  3. For endpoints that let us update parts of the state, but are not idempotent or come with side-effects, we should use the PATCH method.

200 Is Not the Only Success Status

Returning 200 OK for every successful response is, well, ok! But there are some better semantics you could use.

For example, the 201 Created code is a pretty neat specifier to use when responding to a create request. This is also very useful if you are using PUT to allow both create/update requests because it allows you to distinguish which of the two operations was performed. For example:

  • The first call to PUT /item/my-cool-item will result in 201 Created.
  • Follow-up PUT request to update the item will return a 200 OK.

The other interesting success code is 202 Accepted which indicates that the request has been accepted for processing, but the processing has not been completed. It’s a noncommittal response that will require the client to follow up and monitor the status. It’s useful when dealing with APIs that provide wrapper functions, for example:

  1. You request an async job run and get the response 202 Accepted { id: 12345 }
  2. You then query GET /job-status/12345 to check on the status.

Client Errors for Resource Updates

Suppose two users, Alice and Bob, both load a resource with an id 12345 and version 1 and they want to update it. Alice submits her request first and it gets accepted. Then Bob submits his and gets rejected because he’s trying to submit a version 2 that already exists.

What would be the correct response code? Semantically, 400 Bad Request is not a complete description of what happened. A better and more explicit code would be 409 Conflict.

But we can go a step further. If you are using entity tags (ETags), then you get two powerful new semantic statuses. Before I explain these, a quick primer on the idea of an entity tag if you are unfamiliar with these.

Entity tags are lightweight headers that provide an identifier for a specific version of a resource. The tag is usually a hash.

In our example from earlier, this means that when Alice and Bob load the same resource it would also with the same ETag value, e.g. 23ccbc9e-eacb-11ed-a05b-0242ac120003. After Alice edited the resource, the ETag of the resource will change since it is now a new version. Let’s say this is a new value such as 5f851db5-3c9a-44e4-ae8e-2a993c73dd4d.

When Bob sends his request to update the resource, the client will include the previous ETag value in an If-Match header. The server will first validate if this matches the existing resource. Since it does not, the server doesn’t have to waste any more time on parsing or validating the request. It can simply reject it right away with the status code 412 Precondition Failed.

But what if Bob’s client is out of date or buggy and doesn’t include the If-Match header? Then you can use the 428 Precondition Required response.

Is It Not Found or Is It Gone?

A 404 Not Found error makes it clear that the requested resource was not found. But that still leaves two other questions:

  1. Could this be temporary? If I retry in a few hours will it be back?
  2. Did this ever exist in the past? Was it deleted?

We can’t really provide much of an answer to #1, but if you are sure about #2 and want to provide an explicit answer, then the status code 410 Gone is a good choice.

An example where this might be useful is when you allow users to delete their account but want to preserve the notion that they were once registered and prevent others from claiming their username.

Hiding Project Deadlines within Not Implemented

To close off, here’s something that is not useful but shows a cool and fun semantic. Let’s say you created an API endpoint at POST /my-cool-feature, but there’s no handler code to actually process the request.

You can return the response 501 Not Implemented to anyone who calls it. But you can have a bit more fun. If you expect the endpoint to be implemented at a specific date, you can also return a Retry-After header with your launch date!

Filip Danic

Filip Danic

Software Development Manager at Amazon and recovering JavaScript enthusiast. Sharing my experience and insights with the broader tech community via this blog!
The Netherlands