When developing an API that is for internal use, meaning, the API will not be used by external clients and applications but only by one single domain/client, we rarely take care of unified format of responses, particularly for POST, PATCH and PUT requests. Also, not infrequently, we struggle with time and people during the development process, and if you are a senior developer or lead, you have probably struggled with this topic so far.
Consider this. You have a project, start-up or not, where you have a small team which will need to provide an MVP version for a couple of weeks to convince the client that your team is capable to develop something serious like the planned project. Without the need to comment this situation, you probably already guess how this will end up. It will end up with a functional application with messy code and poorly planned functionalities. It’s OK, we’ve all been through something similar or will be soon. 🙂 My point is that you will get to the point where you will need to refactor the previous code or work on a new project and will undoubtedly want to avoid some of the previously learned lessons.
When it comes to responses, if we do not reach an agreement at the start of the development process and do not begin creating technical documentation for the request/response cycle, we will most likely end up with an API that has no standards that we as clients are aware of. The issue will worsen when our API is required to be used by mobile applications as well (still the same domain, but we now have different clients). Clients will expect some standard in order to properly handle all successful and error responses, which may be a problem for POST, PATCH, and PUT requests in particular.
I will try to give you a tip that helped me a lot on this topic with developing and using custom classes for successful and error responses using PHP as a programming language with the intention of using it within the Laravel framework, but the concept could be used in any technology.
Successful responses
Let’s start with the successful responses.
namespace App\Api\Shared\Responses;
use App\Interfaces\SuccessfulResponseInterface;
use Illuminate\Http\JsonResponse;
class Success
{
/**
* Returns json response needed in controllers.
*
* @param string|SuccessfulResponseInterface $data Data need to be
* contained in the response.
* @param integer $code Response status code.
* @param array $headers Headers needed to be set for the response.
* @param integer $options Options.
*
* @return JsonResponse
*
* @throws Exception Throws an exception when status code does not
* correspond or message property is not set in response data object.
*/
public static function response(
string|SuccessfulResponseInterface $data,
int $code = 200,
array $headers = [],
$options = 0
): JsonResponse {
if ($code < 200 || $code >= 300) {
throw new Exception('Status code is invalid');
}
if ($data instanceof SuccessfulResponseInterface) {
$dataArray = get_object_vars($data);
if (!array_key_exists('message', $dataArray)) {
throw new Exception('Message property does not exist');
}
foreach ($dataArray as $key => $value) {
$response[$key] = $value;
}
return response()->json($response, $code, $headers, $options);
}
return response()->json(
[
'message' => $data
],
$code,
$headers,
$options
);
}
}
You can see that the class for successful response contains an argument called $data
which can be a simple string
that will be took into consideration as a response message or it can be an implementation of the SuccessfulResponseInterface
which we will need to create. This means that we can send a response with a simple message or send an object with different properties which all need to be transformed as a response properties. As you can see, I’m also validating the status code and the message property of the response data argument, but this is only for learning purposes, so I suggest creating separate exception classes for all different cases you will handle here.
Lets create the SuccessfulResponseInterface
.
namespace App\Interfaces;
interface SuccessfulResponseInterface
{
public function getMessage(): string;
public function setMessage(string $message): void;
}
As you can see from the interface, all implementations classes will need to implement getMessage
and setMessage
methods, which will make the developer to follow the rule that all responses will need to have the message property.
Now you can use this class like this
return Success::response('Your record was successfully processed.');
or you can send a custom object with custom properties (you will need to create custom class implementation of the above interface) with custom status code, headers, etc.
Error responses
Now let’s create the error response class.
namespace App\Responses;
use App\Interfaces\ErrorResponseInterface;
use Illuminate\Http\JsonResponse;
class Error
{
/**
* Returns json response needed in controllers.
*
* @param string|ErrorResponseInterface $data Data need to be
* contained in the response.
* @param integer $code Response status code.
* @param array $headers Headers needed to be set for the response.
* @param integer $options Options.
*
* @return JsonResponse
*
* @throws Exception Throws an exception when status code does not
* correspond or message property is not set in response data object.
*/
public static function response(
string|ErrorResponseInterface $data,
int $code = 0,
array $headers = [],
$options = 0
): JsonResponse {
if ($code < 400) {
throw new Exception('Status code is invalid');
}
if ($data instanceof ErrorResponseInterface) {
$dataArray = get_object_vars($data);
if (!array_key_exists('message', $dataArray)) {
throw new Exception('Message property does not exist');
}
foreach ($dataArray as $key => $value) {
$response[$key] = $value;
}
return response()->json($response, $code, $headers, $options);
}
return response()->json(
[
'message' => $data
],
$code,
$headers,
$options
);
}
}
As you can see, the Error
class is very similar to the Success
class in terms of logic and way of functioning, with a difference in the status code number handling that makes the status code mandatory. This class also can be used with a single string which will be used as an error message property in the response or a separate ErrorResponseInterface
implementation.
The
will look like thisErrorResponseInterface
namespace App\Interfaces;
interface ErrorResponseInterface
{
public function getMessage(): string;
public function setMessage(string $message): void;
}
As you can see, very similar to the class itself, it is identical to the SuccessfulResponseInterface
. This might give you an idea for making a single way of handling responses with a single class, but I suggest creating separate classes for successful and error responses which will be better approach regarding visual cleanliness when using these classes and easier future extending and maintaining.
Now, the Error class can be used like this
return Error::response('Your record was not processed successfully!', 400);