Hello everyone, hope you are doing well and staying safe.
When designing the API, one of the most important things is to design the API in a way that, Response from the API calls should be uniform and predictable, So that the users of API can rely on it without any fear of incompatibility across API Calls. I recently designed the API using Laravel.
I created a bunch of methods, which are used to return the response. The response may be of any type but the core structure stays the same. Whether it's an error, success response, Validation Error, or anything. I am just importing that trait in Controller.php.
ApiResponseTrait.php
<?php
/**
* Created by PhpStorm.
* User: Bawa, Lakhveer
* Email: iamdeep.dhaliwal@gmail.com
* Date: 2020-06-14
* Time: 12:18 p.m.
*/
namespace App\Http\Traits\Helpers;
use App\Http\Resources\Ghost\EmptyResource;
use App\Http\Resources\Ghost\EmptyResourceCollection;
use Error;
use Exception;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Resources\Json\JsonResource;
use Illuminate\Http\Resources\Json\ResourceCollection;
use Illuminate\Validation\ValidationException;
trait ApiResponseTrait
{
/**
* @param JsonResource $resource
* @param null $message
* @param int $statusCode
* @param array $headers
* @return JsonResponse
*/
protected function respondWithResource(JsonResource $resource, $message = null, $statusCode = 200, $headers = [])
{
// https://laracasts.com/discuss/channels/laravel/pagination-data-missing-from-api-resource
return $this->apiResponse(
[
'success' => true,
'result' => $resource,
'message' => $message
], $statusCode, $headers
);
}
/**
* @param array $data
* @param int $statusCode
* @param array $headers
* @return array
*/
public function parseGivenData($data = [], $statusCode = 200, $headers = [])
{
$responseStructure = [
'success' => $data['success'],
'message' => $data['message'] ?? null,
'result' => $data['result'] ?? null,
];
if (isset($data['errors'])) {
$responseStructure['errors'] = $data['errors'];
}
if (isset($data['status'])) {
$statusCode = $data['status'];
}
if (isset($data['exception']) && ($data['exception'] instanceof Error || $data['exception'] instanceof Exception)) {
if (config('app.env') !== 'production') {
$responseStructure['exception'] = [
'message' => $data['exception']->getMessage(),
'file' => $data['exception']->getFile(),
'line' => $data['exception']->getLine(),
'code' => $data['exception']->getCode(),
'trace' => $data['exception']->getTrace(),
];
}
if ($statusCode === 200) {
$statusCode = 500;
}
}
if ($data['success'] === false) {
if (isset($data['error_code'])) {
$responseStructure['error_code'] = $data['error_code'];
} else {
$responseStructure['error_code'] = 1;
}
}
return ["content" => $responseStructure, "statusCode" => $statusCode, "headers" => $headers];
}
/*
*
* Just a wrapper to facilitate abstract
*/
/**
* Return generic json response with the given data.
*
* @param $data
* @param int $statusCode
* @param array $headers
*
* @return JsonResponse
*/
protected function apiResponse($data = [], $statusCode = 200, $headers = [])
{
// https://laracasts.com/discuss/channels/laravel/pagination-data-missing-from-api-resource
$result = $this->parseGivenData($data, $statusCode, $headers);
return response()->json(
$result['content'], $result['statusCode'], $result['headers']
);
}
/*
*
* Just a wrapper to facilitate abstract
*/
/**
* @param ResourceCollection $resourceCollection
* @param null $message
* @param int $statusCode
* @param array $headers
* @return JsonResponse
*/
protected function respondWithResourceCollection(ResourceCollection $resourceCollection, $message = null, $statusCode = 200, $headers = [])
{
// https://laracasts.com/discuss/channels/laravel/pagination-data-missing-from-api-resource
return $this->apiResponse(
[
'success' => true,
'result' => $resourceCollection->response()->getData()
], $statusCode, $headers
);
}
/**
* Respond with success.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondSuccess($message = '')
{
return $this->apiResponse(['success' => true, 'message' => $message]);
}
/**
* Respond with created.
*
* @param $data
*
* @return JsonResponse
*/
protected function respondCreated($data)
{
return $this->apiResponse($data, 201);
}
/**
* Respond with no content.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondNoContent($message = 'No Content Found')
{
return $this->apiResponse(['success' => false, 'message' => $message], 200);
}
/**
* Respond with no content.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondNoContentResource($message = 'No Content Found')
{
return $this->respondWithResource(new EmptyResource([]), $message);
}
/**
* Respond with no content.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondNoContentResourceCollection($message = 'No Content Found')
{
return $this->respondWithResourceCollection(new EmptyResourceCollection([]), $message);
}
/**
* Respond with unauthorized.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondUnAuthorized($message = 'Unauthorized')
{
return $this->respondError($message, 401);
}
/**
* Respond with error.
*
* @param $message
* @param int $statusCode
*
* @param Exception|null $exception
* @param bool|null $error_code
* @return JsonResponse
*/
protected function respondError($message, int $statusCode = 400, Exception $exception = null, int $error_code = 1)
{
return $this->apiResponse(
[
'success' => false,
'message' => $message ?? 'There was an internal error, Pls try again later',
'exception' => $exception,
'error_code' => $error_code
], $statusCode
);
}
/**
* Respond with forbidden.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondForbidden($message = 'Forbidden')
{
return $this->respondError($message, 403);
}
/**
* Respond with not found.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondNotFound($message = 'Not Found')
{
return $this->respondError($message, 404);
}
// /**
// * Respond with failed login.
// *
// * @return \Illuminate\Http\JsonResponse
// */
// protected function respondFailedLogin()
// {
// return $this->apiResponse([
// 'errors' => [
// 'email or password' => 'is invalid',
// ]
// ], 422);
// }
/**
* Respond with internal error.
*
* @param string $message
*
* @return JsonResponse
*/
protected function respondInternalError($message = 'Internal Error')
{
return $this->respondError($message, 500);
}
protected function respondValidationErrors(ValidationException $exception)
{
return $this->apiResponse(
[
'success' => false,
'message' => $exception->getMessage(),
'errors' => $exception->errors()
],
422
);
}
}
Now We Will Update Exceptions\Handler.php which is responsible for handling all kind of exceptions
Handler
Handler.php
<?php
namespace App\Exceptions;
use App\Http\Traits\Helpers\ApiResponseTrait;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Database\Eloquent\ModelNotFoundException;
use Illuminate\Database\QueryException;
use Illuminate\Foundation\Exceptions\Handler as ExceptionHandler;
use Illuminate\Http\Exceptions\PostTooLargeException;
use Illuminate\Http\Exceptions\ThrottleRequestsException;
use Illuminate\Validation\ValidationException;
use Throwable;
class Handler extends ExceptionHandler
{
use ApiResponseTrait;
/**
* A list of the exception types that are not reported.
*
* @var array
*/
protected $dontReport = [
//
];
/**
* A list of the inputs that are never flashed for validation exceptions.
*
* @var array
*/
protected $dontFlash = [
'password',
'password_confirmation',
];
/**
* Report or log an exception.
*
* @param \Throwable $exception
*
* @return void
*
* @throws \Exception
*/
public function report(Throwable $exception)
{
$ignoreable_exception_messages = ['Unauthenticated or Token Expired, Please Login'];
// $ignoreable_exception_messages[] = 'The refresh token is invalid.';
$ignoreable_exception_messages[] = 'The resource owner or authorization server denied the request.';
if (app()->bound('sentry') && $this->shouldReport($exception)) {
if (!in_array($exception->getMessage(), $ignoreable_exception_messages)) {
app('sentry')->captureException($exception);
}
}
parent::report($exception);
}
/**
* Render an exception into an HTTP response.
*
* @param \Illuminate\Http\Request $request
* @param \Throwable $exception
*
* @return \Symfony\Component\HttpFoundation\Response
*
* @throws \Throwable
*/
public function render($request, Throwable $exception)
{
if ($request->expectsJson()) {
if ($exception instanceof PostTooLargeException) {
return $this->apiResponse(
[
'success' => false,
'message' => "Size of attached file should be less " . ini_get("upload_max_filesize") . "B"
],
400
);
}
if ($exception instanceof AuthenticationException) {
return $this->apiResponse(
[
'success' => false,
'message' => 'Unauthenticated or Token Expired, Please Login'
],
401
);
}
if ($exception instanceof ThrottleRequestsException) {
return $this->apiResponse(
[
'success' => false,
'message' => 'Too Many Requests,Please Slow Down'
],
429
);
}
if ($exception instanceof ModelNotFoundException) {
return $this->apiResponse(
[
'success' => false,
'message' => 'Entry for ' . str_replace('App\\', '', $exception->getModel()) . ' not found'
],
404
);
}
if ($exception instanceof ValidationException) {
return $this->apiResponse(
[
'success' => false,
'message' => $exception->getMessage(),
'errors' => $exception->errors()
],
422
);
}
if ($exception instanceof QueryException) {
return $this->apiResponse(
[
'success' => false,
'message' => 'There was Issue with the Query',
'exception' => $exception
],
500
);
}
// if ($exception instanceof HttpResponseException) {
// // $exception = $exception->getResponse();
// return $this->apiResponse(
// [
// 'success' => false,
// 'message' => "There was some internal error",
// 'exception' => $exception
// ],
// 500
// );
// }
if ($exception instanceof \Error) {
// $exception = $exception->getResponse();
return $this->apiResponse(
[
'success' => false,
'message' => "There was some internal error",
'exception' => $exception
],
500
);
}
}
return parent::render($request, $exception);
}
}
Top comments (9)
Nice. I use Laravel Responder for this. It also returns uniform response for Laravel base response or errors like firstOrFail(), abort() etc.
You can check it out here github.com/flugger/laravel-responder
Hmm, that's also not bad, But I guess, to use that, We must be using Fractal Package, which is no longer required with the New Version of Laravel as we already have the functionality of Fractal in laravel core now
Can you point me to that?
I believe Laravel response has the dependency on github.com/thephpleague/fractal which is no longer required as We have Laravel Api Resources now
laravel.com/docs/8.x/eloquent-reso...
Oops, didn't know that's what you are referring to. Its been there for a while. Since 5.5
I fractal works for me.
yeah, thats true
is this handler also can use to handling the normal (web) request
Updated the code with one I am using, Yes, I should handle web request normally now
That's great, thank you very much.
One thing is that you are providing missing documents
EmptyResource,
EmptyResourceCollection