guanguans/laravel-api-response
Laravel package for building consistent JSON API responses. It provides helpers and macros for uniform success and error envelopes with customizable code, message, data, and HTTP status, plus exception rendering to keep API output predictable.
Install the package and publish its config so you can see the response envelope before writing any code:
composer require guanguans/laravel-api-response
php artisan vendor:publish --tag=api-response-config
The first place to look is config/api-response.php. It defines the envelope keys (code, message, data), the default success/failure codes and messages, and any wrapping behaviour. That file is the contract your frontend will depend on, so agree on it before building endpoints. After that, read the facade (Guanguans\LaravelApiResponse\Facades\ApiResponse) and the Response class it returns — the facade is the primary entry point, and the Response object is what Laravel serialises.
The first use case is almost always "make one endpoint return a predictable shape":
use Guanguans\LaravelApiResponse\Facades\ApiResponse;
Route::get('/ping', fn () => ApiResponse::success(['pong' => true]));
Which produces the standard envelope:
{ "code": 200, "message": "OK", "data": { "pong": true } }
Then prove the failure path works too:
return ApiResponse::failure('Resource not found', 404);
If both of those render as expected, the package is wired up correctly and you can standardise the rest of the API around it.
Use the response object as a controller return value. The returned Response implements Laravel's Responsable contract, so you can return it directly from controllers, route closures, and form requests. This keeps controllers free of response()->json([...]) boilerplate:
public function show(User $user)
{
return ApiResponse::success(new UserResource($user));
}
Add thin helpers to a base controller rather than repeating message strings and codes:
protected function ok(mixed $data = null, string $message = 'OK')
{
return ApiResponse::success($data, $message);
}
protected function fail(string $message, int $code = 400, mixed $data = null)
{
return ApiResponse::failure($message, $code, $data);
}
Centralise exception rendering. Map domain exceptions to envelope responses in bootstrap/app.php (Laravel 11+) or app/Exceptions/Handler.php (older). This is where the package earns its keep — every uncaught exception gets the same shape as successful responses:
$this->renderable(function (Throwable $e, Request $request) {
if ($request->expectsJson()) {
return ApiResponse::failure(
app()->isProduction() ? 'Internal Server Error' : $e->getMessage(),
$e instanceof ModelNotFoundException ? 404 : 500
);
}
});
Normalise validation failures in the same handler so clients always parse code / message / data. ValidationException should return 422 with the errors under data (or a dedicated errors key), not a raw Laravel error payload.
Use attributes for per-endpoint overrides if the package version you install exposes them — they keep status codes and messages next to the action instead of buried in a config array:
#[ApiResponse(statusCode: 201, message: 'Created')]
public function store(StoreUserRequest $request) { /* ... */ }
Check the installed version's attribute namespace; this is the part of the API most likely to shift between releases.
Pass paginators and resources through the package, not around it. Returning ApiResponse::success($users) where $users is a paginator lets the package merge data and pagination meta. If you manually call ->items() you'll lose the meta unless you add it yourself.
Assert the envelope in tests. A single assertion trait or helper keeps the whole suite consistent:
$this->getJson('/api/users')
->assertOk()
->assertJsonPath('code', 200)
->assertJsonStructure(['code', 'message', 'data']);
Treat the envelope as a frontend contract. Once agreed, client interceptors only need to branch on HTTP status and the business code. Document both, because they are not the same thing.
code describes domain outcome. Decide early whether code mirrors HTTP or is a business code (e.g. 10001). Mixing them makes clients branch on both inconsistently.JsonResource inside ApiResponse::success() is fine; returning $resource->response() wraps the already-serialised response again. Pick one path per endpoint.api group only, and make it idempotent.catch (Throwable) that returns 200 hides real 500s and breaks monitoring. Re-throw unknown exceptions after logging, or at least preserve the original status.$e->getMessage() on app()->isProduction(); the package will faithfully render whatever you give it.data: [], data: null, and an omitted data key are three different contracts. Pick one and assert it in tests — frontends often break on the difference.config/api-response.php, run php artisan config:clear. In CI, always clear before running tests.message actually contains in the JSON.dd($response->toArray()) (or ->getData(true)) before it hits the HTTP layer; and log the original throwable in the handler before converting it, or you lose the stack trace.composer.json and read the changelog before upgrading — attribute names and helper methods are the usual breaking points.How can I help you explore Laravel packages today?