andrey-helldar/laravel-json-response
A Laravel package for building consistent JSON API responses with a fluent helper, standard success and error payloads, HTTP status codes, headers, and resource support, simplifying controller output and keeping API responses uniform.
Install and confirm the surface area first.
composer require andrey-helldar/laravel-json-response
php artisan about # sanity-check the framework version resolved
The package is a conventional Composer library with Laravel auto-discovery, so there is no service-provider registration step. Before writing any code, spend ten minutes in vendor/andrey-helldar/laravel-json-response/:
src/ — the package is deliberately small. The whole feature surface is one or two classes plus a facade/trait. Read them end to end; that is the real documentation.composer.json — confirm the illuminate/* constraint and the minimum PHP version. 3.2.0 is the release that adds Laravel 13 support, so pin ^3.2 if you're on 13 and don't expect earlier 3.x to work there.config/ — if a config file ships, publish it and read the keys before you rely on defaults. If nothing ships, there's nothing to publish; don't hunt for a provider that isn't there.The repository is archived. Treat the source in
vendor/as the authoritative spec, not the README, and expect no upstream fixes. That doesn't disqualify it — the scope is small enough to fork — but it changes how you consume it (see Gotchas).
First use case: make one endpoint consistent, then copy it.
Pick a single controller method that currently returns response()->json([...]) by hand. Replace it with the package helper, confirm the payload shape and status code in the browser or via php artisan route:list + curl, then standardise the rest of the controller around that shape. Don't refactor the whole API in one pass — the value here is a convention, and the convention is easier to agree on when you can see one endpoint rendered.
// before
return response()->json(['data' => $user, 'message' => 'OK'], 200);
// after (illustrative — verify exact helper names in src/)
return json_response()->success($user);
Given the archived status, do not let the package's helper names leak into every controller. Create App\Support\ApiResponse with ok(), created(), noContent(), fail(), notFound(), validationFailed() methods that internally call the package. You get:
ApiResponse::) instead of a global helper.This is cheap insurance and takes fifteen minutes.
abstract class ApiController extends Controller
{
protected function ok(mixed $data = null, string $message = 'OK'): JsonResponse
{
return ApiResponse::ok($data, $message);
}
protected function created(mixed $data): JsonResponse
{
return ApiResponse::created($data);
}
}
Every API controller extends it. New endpoints inherit the shape for free; a reviewer can spot an inconsistent response by the absence of the base class.
The failure mode with hand-rolled JSON is response()->json($data, 200) on an error path. Give the wrapper named methods per status (notFound(), forbidden(), unprocessable()) so the status is chosen by the method name, not by a magic number a developer has to remember.
return $this->notFound('User not found');
// not: response()->json(['error' => '...'], 404)
The highest-leverage integration is bootstrap/app.php (Laravel 11+) / App\Exceptions\Handler. Register renderable callbacks for ValidationException, AuthenticationException, ModelNotFoundException, and AuthorizationException that return your envelope via the wrapper. Now framework-thrown errors and your controllers produce the same JSON — which is exactly the promise the package makes, and the thing hand-rolled responses always get wrong.
->withExceptions(function (Exceptions $exceptions) {
$exceptions->render(fn (ValidationException $e) => ApiResponse::validationFailed($e->errors()));
})
return $this->ok(UserResource::collection($users));
Do not pass a paginator and a data key — you'll get {"data": {"data": [...]}}. If the package doesn't handle paginators, call ->toArray() on the resource or use ->response()->getData(true) and pass that through once, deliberately, in the wrapper.
Write one test that asserts the envelope shape, then reference it from everywhere:
$response->assertOk()->assertJsonStructure(['data', 'meta', 'message']);
$response->assertJsonPath('data.id', $user->id);
$response->assertStatus(422)->assertJsonStructure(['errors' => ['email']]);
Use $this->getJson() / $this->postJson() so Laravel sends Accept: application/json — otherwise validation failures return a redirect and your assertion misfires in a way that looks like a package bug.
meta block earlyIf you're going to carry request_id, pagination counts, or API version, decide it now and put it in the wrapper. Retrofitting a meta key across forty endpoints later is the expensive version of this decision.
The repository is archived — pin it and own the fork path. Use an exact or tightly-bounded constraint ("^3.2" is fine; "*" is not). Because it's MIT and a handful of files, a fork is a realistic escalation path at the next Laravel major. That's an argument for the internal-wrapper pattern above, not an argument against the package.
Version 3.2.0 is a compatibility release, not a feature release. If you're staying on Laravel 12, upgrading from 3.1 is inert — absorb it at your next routine dependency refresh. If you're going to Laravel 13, 3.2.0 is the floor. Check the resolved composer.lock diff; the realistic failure is a resolver conflict, not a runtime break.
Empty collections serialise as [], not {}. ['data' => []] becomes {"data":[]}. If your clients switch on the type of data, cast empty arrays to (object) [] or use JSON_FORCE_OBJECT selectively. This bites everyone once.
204 No Content must not carry a body. If the helper always emits a payload, don't use it for DELETE. Return response()->noContent() directly, and note in the base controller that it's the one intentional exception.
Don't mix the helper and response()->json() in the same controller. Pick one. Half-migrated controllers are where inconsistent payloads come from, and reviewers stop noticing after the third file.
Check for double-wrapping after adding API Resources. The most common post-adoption bug is {"data":{"data":...}} — the resource adds its own data key and the helper adds another. Assert the top-level keys in a test; it takes one line and catches it immediately.
Debug by inspecting the response object, not by dd() in the controller. $response->getData(true) gives you the array, $response->getStatusCode() the code, $response->headers->get('Content-Type') confirms JSON. In feature tests, $response->dump() is faster than re-reading the wrapper.
Prefer constructor injection of the response class over a global helper in services. Global helpers are convenient in controllers but awkward to fake in unit tests. If the package exposes both a facade/helper and an injectable class, inject it in anything that isn't a controller.
Extension points are limited by design. If a macro or config key exists, use it; otherwise extend your own ApiResponse wrapper rather than subclassing the vendor class. Never edit vendor/ — with an archived upstream, a lost composer install is unrecoverable.
Confirm the method names before you copy any snippet. The package is small and its README may lag the code. Ten minutes in src/ prevents a morning of chasing helper names that don't exist.
How can I help you explore Laravel packages today?