On APIs and their responses

·

Since the dawn of times the web, humans created CRUD APIs. And we were instructed that modification verbs should return the modified resource in response. But, should they?

A basic CRUD API consists of 5 verbs. Let’s go over them quickly.

The 5 verbs

GET

The GET verb is used to retrieve the resource, or a collection of resources. It should not make any modification on the server, and therefor it’s idempotent.

POST

The POST verb is used to create a resource on the server. It also can be used to create a collection of resources. From what you understood, POST changes the state on the server, and is not idempotent.

PUT

Contrary to POST, PUT is used to replace a resource, or a collection or resources. Like POST, PUT modified the state on the server. However, unlike POST, PUT is idempotent.

PATCH

PATCH is not a well known verb. Some confuse PATCH with PUT, but there is a difference. While PUT replaces the entire resource, PATCH is designed to update parts of a resource.

To give you a better example, imagine we have the following resource:

const resource = { email: "contact@yieldcode.blog", handle: "@skwee357" };

In order to update the email, I have two options.

  • I can issue a PUT request. But this will require me to submit both email and handle fields, because PUT used to replace the resource.
  • I can issue a PATCH request. This way, I can submit a partial JSON that will contain only the new email.

PATCH, as you probably guessed, modifies the state on the server. However, if you assumed that PATCH is idempotent, just like PUT, you are wrong. I won’t go into details here, but you can check this 8-year-old StackOverflow question, where I asked about the same thing.

DELETE

Last, but not least, is DELETE. It’s used to delete a resource from the server. It modifies the state on the server, and is idempotent.

On POST, PUT, and PATCH and their responses

There is a verbal agreement that POST, PUT, and PATCH operations should return the created/updated resource. In the rest of this article, I want to provide reasons why they shouldn’t.

Reason 1: API consistency

Consistent APIs are easier to work with. It doesn’t matter if you plan on third-parties to integrate with your API, or it’s used internally by your frontend/mobile teams.

By returning a newly created resource inside POST request—you will have to return a response in all POST requests. Otherwise, the people who integrate with your API, will have to handle endpoint-by-endpoint use-cases.

Returning response in every POST endpoint is, however, unfeasible. Why? Here are some examples.

  • What would your return on POST endpoint that accepts batch data from CSV? Let’s say you are able to process the CSV in-place, without delayed processing. Are you going to return all the newly created resources? What if there are hundreds of them? Do you return them paginated?
  • What would you return on a POST endpoint that schedules an async job? Let’s say you send s CSV, but instead of parsing it in-place, you schedule the parsing for a later stage using some queue. Do you return a job object? A job ID? Do you even want to expose your API consumers to the concept of asynchronous processing?
  • Imagine you have a resource such as blog post. This blog post can have tags. After issuing a PATCH request to add a new tag, what would you return? Do you return the entire blog post object, or only the array of tags? Or maybe the newly created tag?

As I demonstrated, there are some ambiguities you need to solve in order to support returning the resource on create/update. This complicates your API, and makes it harder for your consumers.

What is the alternative? There is one, but first, let’s discuss some more issues with returning resources.

Reason 2: Offline support

One reason to return the resource on create/update operations—is to avoid the additional GET call to retrieve the newly created/updated resource, that the client will have to do. However, when designing an API to be consumed by a client that can work in offline mode, the client needs to backfill the newly created resource anyway.

When a user creates a resource, but the application is offline, you want to have optimistic flow. The UI updates accordingly, but the resource is queued to be created when the client goes back online. In that case, the application already knows the structure of the POST response. Hence, it’s unnecessary to return anything when issuing create/update.

When the client will be back online, it will execute all the create/update operations to let the server know what happened, and will issue a GET request to update the view for the user.

But Dmitry, I don’t care about offline! Too bad. But let’s say you really don’t care about offline. In today’s world, of zero patience, users expect stuff to happen instantly. And even if you issue a POST request, and wait for a response from it, there could be a noticeable latency that your users won’t like.

One way to fix this latency, is to do an optimistic update. The user fills the form, and click submit. Your UI framework creates a dummy resource and immediately appends it to the list of resources, as if it was created successfully, hence updating the UI. The user is happy.

In the background, you issue a POST request to create the resource on the server. There is a high probability that the request succeeds, hence the name—optimistic update. If it fails, we can notify the user via a pop-up, or retry again up to a certain number of retires.

Reason 3: It violates DRY

DRY, which is an abbreviation for don’t repeat yourself, is a principle that states that your code should be free of repetitions. Repetition make the code hard to maintain, as now there are few places that do the same, or depend on the same method.

By introducing a response to create/update operations, you violate DRY, because you have to return the same resource as the one you return in GET. And if the resources are different between create/update and GET calls, your API becomes inconsistent, which violates the first reason about API consistency.

Reason 4: It violates SRP

SRP, which is an abbreviation for single responsibility principle, states that each module in your code should be responsible for one action. GET returns the resource. DELETE removes the resource. But when you introduce response to create/update operations, they no longer have single responsibility. This can get complicated.

Consider a use-case of adding a new tag to a blog post. The entire logic (excluding authentication and validation), can be narrowed to one particular SQL:

INSERT INTO blog_post_tags (post_id, tag) VALUES ($1, $2) ON CONFLICT IGNORE;

So when your POST operation finishes, you don’t hold the entire blog post resource. Therefore, in-order to return a proper response during POST, you either need to call GET on the same resource internally (many frameworks support issuing internal HTTP requests). Or you need to refactor the resource retrieval, and serialization, to an external function which both the GET and the POST handlers will call.

Now, they need not only create or update the said resource, but also serialize it in the same way as GET would, and return it to the client.

What to do instead

Well, nothing! You just don’t return anything. The same way we do DELETE requests, which return 200 OK. However, for resource creation you should use 201 Created or 202 Accepted if the resource will be created asynchronously.

If you want a really nice API design, you can return a header named Content-Location which will point the client to the location of the newly created resource. This creates a very nice decoupling inside your code, because retrieving the resource becomes a matter of reading this header, rather than hard-coding a specific end-point.

So, instead of

const { id } = await api.post("/task", body);
const task = await api.get(`/task/${id}`);

It becomes less coupled:

const { headers } = await api.post("/task", body);
const task = await api.get(headers["content-location"]);

This, obviously, won’t work for batch operations. While you can use another header, Link, this will quickly get ugly if you have more than a few resources. You can return a Content-Location header that points to the collection, instead of an individual entity. However, I think it’s better to not return anything.

But what about the subsequent GET request? Well, for starters, if you rely on optimistic update (and you should), your UI already reflects the change. There is no need to fetch the created resource.

In general, GET on collection should be performed only once—when the user opens the list of tasks/todos/whatever. GET on a particular resource, should be performed when the user opens the page of that particular resource. In all other cases, you better rely on optimistic update, especially if you have mobile clients with offline capabilities.

And if you really have to fetch the resource, just issue the GET request. It’s not that big of deal.


That’s it. That’s all I wanted to share with you today.

Let me know what do you think about this approach. I’m available via Email, Twitter, Mastodon, and (for the brave corporate folks among you) LinkedIn.

Share this:

Published by

Dmitry Kudryavtsev

Dmitry Kudryavtsev

Senior Software Engineer / Tech Lead / Consultant

With more than 14 years of professional experience in tech, Dmitry is a generalist software engineer with a strong passion to writing code and writing about code.


Technical Writing for Software Engineers - Book Cover

Recently, I released a new book called Technical Writing for Software Engineers - A Handbook. It’s a short handbook about how to improve your technical writing.

The book contains my experience and mistakes I made, together with examples of different technical documents you will have to write during your career. If you believe it might help you, consider purchasing it to support my work and this blog.

Get it on Gumroad or Leanpub


From Applicant to Employee - Book Cover

Were you affected by the recent lay-offs in tech? Are you looking for a new workplace? Do you want to get into tech?

Consider getting my and my wife’s recent book From Applicant to Employee - Your blueprint for landing a job in tech. It contains our combined knowledge on the interviewing process in small, and big tech companies. Together with tips and tricks on how to prepare for your interview, befriend your recruiter, and find a good match between you and potential employer.

Get it on Gumroad or LeanPub