Don't use http-codes

27 June 2022

For the sake of the world, please, stop mixing http-status codes with your code.
I know, it may seem very natural to you to put 400 on any validation issue; or 404 if the resource is not found and so on, but please, hear me out.

HTTP-codes are infrastructure related information.

This is important. When you design an application there is only input and output. It might confuse you to think that status code is output details.
It is, but it resides on a different layer of abstraction. It is a transfer protocol information.
The same thing as other headers. It is just designed for a different purpose.
What happens if your application layer changes communication from http to sockets or command line? Will it also serve 400, 403, 404?

Technical issues.

It might be not obvious but whenever you make an abstraction level mistake, it harms domain and application no matter what.
Placing status code in application layer scenarios always ends up with two different outcomes.

Dead-end scenario.

This means that none of the API consumers will ever handle this case. It is just not intended to happen and consumers just try-catch it with a generic handler. Something like:

axios.interceptors.request.use(() => {}, (error) => {
    if (error.response) {
        // log it, display "error occurred" or whatever
    }

    return Promise.reject(error);
});

It happens because there is usually no particular fallback-scenario for cases that meant not to happen.
Consumers have their own domain that prevents most of these cases most of the time.

Eventually, in this case API could just return some indicator of an error. Which leads us to the second outcome.

Considered scenario.

This one is actually handled. Let's say it is a validation error for an input data.

Backend responds with 400(Bad request)

axios.post('/api/users', payload)
  .catch(function (error) {
    if (error.response?.status == 400) {
        // display "your data is invalid"
    }
  });

That's it. Does this "your data is invalid" help somehow? Might it be we are missing some details?
Here it comes. Now the backend has to share them. Obviously in response content. Let`s see

axios.post('/api/users', payload)
  .catch(function (error) {
    if (error.response?.status == 400) {
        // display error.response.data.errors
    }
  });

Now user can see what exactly is wrong Looks great, right?

Before we go further I want to ask some backend questions.

<?php

function createUserController(): Response {
    // This is the scenario that we handle on client-side in previous example
    if (!isset($payload['email'], $payload['password'])) {
        return new ErrorResponse('Userdata is empty', 400);
    }
    
    $userAlreadyExists = findUserByEmail($payload['email']) !== null;
    if ($userAlreadyExists) {
        // Here is another error for input data.
        // What is it? Is it a bad request(400) or something else? What about more complex scenarios?
    }
    
    // ....
}

So, what is it? Is it a bad request if the user already exists in the system? This one is actually not that troubling.
It will be much more complicated when errors will appear on the domain layer, and you will have to map them somehow to http-codes.
I`m almost sure you are familiar with an API that has dozens of error scenarios.
Billing/payment is the most popular example (invalid cvv, duplicate charge, expired card, declined auth, etc.).

Solution.

Here we are coming close to the outcome of this article.
Don't. Use. HTTP-codes. To represent your application scenarios.

You might have already figured out the better solution for this.
If you need to segregate scenarios and cases, put details into output.

// This is it. The error that contains details without binding to http-protocol.
type SystemMessage = {
  readonly code: number,
  readonly message: string
}

//This one is just a wrapper for response content/body
class Result<Data> {
  data: Data | undefined;
  error: SystemMessage | undefined;

  constructor(data?: Data, error?: SystemMessage) {
    this.data = data;
    this.error = error;
  }

  isSuccessful(): boolean {
    return this.error === undefined;
  }

  getData(): Data {
    if (!this.isSuccessful()) {
      Logger.error('This method was not expected to be called for result is not successful.');
    }

    return this.data as Data;
  }

  getError(): SystemMessage {
    if (this.isSuccessful()) {
      Logger.error('This method was not expected to be called for result is successful.');
    }

    return this.error as SystemMessage;
  }
}

adapter.interceptors.response.use(
  (response) => new Result(response.data),
  (error) => new Result(undefined, error.response.data),
);


axios.post('/api/users', payload)
  .then((res: Result) => {
    if (!res.isSuccessful()) {
        error = res.getError();
      // error .code contains error code. not http but application code(0x0001, 1, 100500)
      // error.message is additional details about the error. it may be string, list of strings or whatever you need
    }
  })

The main idea here is to avoid limiting yourself with http-protocol details.
You can use whatever codes you like with even higher flexibility.
Also, you won't be ought to map domain level error to infrastructure through application. This will save you a lot of nerves.

Have a good one (:

Don't take everything plain: we have to challenge and prove the information we face.

Here is what really helps me to do it.