APIs: When POSTs succeed but do not return a 201

By Will Braynen

Illustration by Donna Blumenfeld

Illustration by Donna Blumenfeld

Design Patterns

Sometimes computers talk to one another.  Fairly often in fact.  To help make these computer conversations meaningful, engineers rely on conventions.  One of these conventions is the http status codes that services return to their clients.  REST APIs follow these http conventions, or at least should.  This article is about a specific and interesting case of that: the case when everything goes according to plan.

 

four POST cases instead of one

When working with or designing a REST API, we typically think of successful GETs as always returning a 200 and successful POSTs always returning a 201.  Like so:

REST verb (http method)Success (http status code)
GET 200 (OK)
POST 201 (Created)

Because GETs work with resources that already exist on the server while POSTs create new ones.  That, however, is not the case.  An http POST request can also, by good design, return a 200, a 202, or even a 204.  Moreover, a 200 does not necessarily mean that a resource was not created.  The fuller picture looks more like this:

REST verb (http method)Success (http status code)
GET 200
POST 200, 201, 202, 204

In fact, it's the service that might be POSTing to the client!  So, really, it's like so:

REST verb (http method) Server's success response Client's success response
GET 200
POST 200, 201, 202 204

Behind each successful status code stands an interesting and useful pattern.  Here goes.

 

CASE 1.  201 (Created)

This is the familiar scenario of a persistent resource.  

You, the client, send new data to the service (using a POST).  The service then creates a new resource that a client can later retrieve (using a GET) or update (using a PUT or a PATCH).  For example, the service inserts this new data into a database as a new row in a table.  The service then returns to the client a resource id—a primary key, for example—so that the client can retrieve or update this resource later.  201.  This is the familiar POST scenario.

 

CASE 2.  200 (OK)

This is the less familiar case of ephemeral resources.  

Resource was received but, per design, cannot be later retrieved directly.  The definition of "ephemeral" is client-centric because status codes are for the API's consumers (the clients), not those who implemented the API.

Scenario 1.  You, the client, send new data to the service and the service acts as a pass through.  The service doesn't save the data but resends it to someone else and who knows what that someone else does with the data.  No resource id is returned to the client with which the client could later GET retrieve the resource directly.  200.  Because the resource was not saved and so cannot be retrieved (or updated) later.  From the point of view of the client, the API's consumer, the resource is not persistent.

Scenario 2.  You, the client, send new data to the service and the service saves it.  But, the service does not give you a way to retrieve the data because the API does not include this functionality (hopefully for a good reason like security).  The service then does not return a resource id to the client.  200.  Because it is not the case that the data was saved and can be retrieved (or updated) later.

Scenario 3.  As part of an "experience API", you have a backend-for-frontend (BFF) service.  You, the client, POST something to the BFF.  The BFF calls a number of downstream services and, acting as an aggregator, gives you one single response.  In this case, the BFF should return a 200 and return more fine-grained per-resource responses in the body of its http response.  One reason for this is because a 201 can be misleading in case of partial successes.  Similarly, you might POST resource x, which means creating resources y1 and y2 with two different microservices.  And even if the BFF today actually creates resource x on the backend, tomorrow its implementation might change.  So, given that a BFF isn't a direct service by definition, its full or partial successes should return a 200.

 

CASE 3.  202 (Accepted)

This is the case of long-running jobs and how the service is expected to respond.

Why receive/deliver results asynchronously.  There are two reasons.  The first is to not keep clients waiting.  The second is to not exhaust the number of connections on the server, which keeping sockets open for too long will do.

Scenario.  The scenario: the request will take a long time to fulfill.  The service creates a long-running job and returns a 202 (Accepted) to the client.  By "a long time", I mean that, from the time the service receives the request to the time the service replies (not including the time it takes for that response to reach the client over the network), it takes longer than half a second or even longer than a quarter of a second.  (For a mobile client, backend response times of more than half a second might translate into having to wait for more than 3 seconds.)

According to RFC 2616, a status code of 202 means that "[t]he request has been accepted for processing, but the processing has not been completed. The request might or might not eventually be acted upon, as it might be disallowed when processing actually takes place. There is no facility for re-sending a status code from an asynchronous operation such as this."

But how does the client receive the results?  The result of the job request has to then be retrieved (by the client) or delivered (by the service) asynchronously. To see this async pattern in its entirety, it is important to understand what happens after the job has been accepted by the service — after the 202.  One possibility is that the service tells the client to call back later to fetch the results of the long-running job by using a GET.  The client would then need a callback url and/or a resource id and would have to know how long the job will take.  In other words, the service says "202 (Accepted), but this will take too long for you to wait around, so call me back in ___ seconds or minutes." The service also tells the client when to call back and gives the client a callback url and/or a resource id. The client calls back later using a GET.  This pattern is common for long-running jobs requested by mobile clients and will not yield a 204.  But another alternative async pattern, common when the client is another service, will.

 

CASE 4.  204 (no Content)

This is the case of long-running jobs and the client's response.  If Case 3 was to be continued, then this is the next possible chapter.  Recall that Case 3 (see above) was about the service telling the client that the job will take too long to just sit around and wait for the results.

How does a client get the results of a long-running job once the service has completed it?  There are two design options, which result in two different scenarios.  The "client calls back later" scenario (or pattern) is described in the previous section.  The "service calls back the client" scenario (or pattern) is described next.

The client is another service.  The client tells the service, "Here's is my job request, but I know it will take too long for me to wait around, so just call me back once you are done at ____ url."  The service agrees and so returns a 202 (Accepted).  The service calls back the client later to POST the results of the job to the client.  The client responds to the service with a 204 (No Content) because, after all, it's the service that's trying to give the client something; the service shouldn't expect anything from the client (other than the 204 success code).

If the client is another service, this pattern can work because the client can have its own http server listening on a port at said url.  But if it's a mobile client — an iOS app for example — then there is no one for the service to call back.  If so, then the client has to call the service back, as described in the 202 section above.

One alternative to Case 3 and Case 4 design patterns would be to use a WebSocket.  This would mean keeping a socket connection open, but still having an asynchronous pattern by not holding up the client thread.

 

Summary

REST verb + resource type Service's success response to a client's POST Client's success response to a service's POST
GET a persistent resource (including the result of a long-running job) 200 (even if it's a long-running job that has not yet been completed)
POST an ephemeral resource (including BFF) 200
POST a persistent resource 201
POST a long-running job, design pattern 1 202 (Service replies to client's request with a 202; client should come back later to GET the result.)
POST a long-running job, design pattern 2 202 (Service replies to client's request with a 202 and will call client once job is complete.) 204 (Service POSTed result to client; client replied with a 204.)

 

Clashing Nomenclature

If you want to keep the understanding you already have, then stop reading here.  But if you want a brain twister and some confusion injection, then this clarificatory concluding section is for you.

Nomenclature One: client and service. That is, an API service and an API client. The word service refers to a monolith or microservice that implements a REST(ful) API.  The word client refers to the client that consumes this API.  In this article, I used the word "client" in this sense.

Nomenclature Two: client and server. That is an HTTP client and an HTTP server.  This refers to a client-server model.  Interactions between API clients and services also follow a client-server model because REST calls are HTTP requests and HTTP requests involve opening a socket connection between the client and the service.  Typically, the service uses an HTTP server and the API client uses an HTTP client. That's because, typically, the client calls the service. However, as we saw, the case of a 204 is not typical. When the service calls back the client, the service becomes a client and the client becomes a server. In the client-server model sense, in the case of a 204, the client and the server swap roles.