Every Laravel project I have worked on hits the same point eventually. One controller returns ['data' => $result]. Another returns ['success' => true, 'payload' => $result]. A third one throws a ModelNotFoundException that Laravel helpfully turns into a stack trace in development and a cryptic 500 in production. The shapes accumulate.
It is not that developers are careless. The framework does not enforce a response shape, so each feature gets the shape its author preferred that day. Two months in, the API consumer is writing normalisation code to handle whatever format each endpoint happens to use.
What a consistent envelope looks like
The simplest version has four keys:
{
"success": true,
"message": "User retrieved",
"data": { ... },
"errors": null
}
Error response:
{
"success": false,
"message": "Validation failed",
"data": null,
"errors": { "email": ["The email field is required."] }
}
Every response — success or failure — has the same outer shape. The consumer checks success, reads data or errors, and moves on. No defensive checking for payload vs data vs result.
Implementing it with a global helper
The cleanest Laravel approach is a global api() helper that returns a response builder. Controllers call it like they would call response()->json(), but without encoding any opinion about structure:
// GET /users
return api()->success(User::all()->toArray());
// POST /users
return api()->created($user->toArray());
// GET /users/{id} — not found
return api()->notFound();
// Validation failed
return api()->validationError($validator->errors());
// Catch-all in the exception handler
return api()->exception($e);
Each method maps to its conventional HTTP code. created() returns 201. notFound() returns 404. validationError() returns 422. No controller needs to remember which code maps to which scenario — the name carries the intent.
Handling exceptions in one place
The biggest reliability win is the exception handler. Without a standard response layer, app/Exceptions/Handler.php often grows into a maze of instanceof checks, each formatting a slightly different JSON structure. With a standard helper:
// app/Exceptions/Handler.php
public function render($request, Throwable $e): Response
{
if ($request->expectsJson()) {
return api()->exception($e);
}
return parent::render($request, $e);
}
One line. Every JSON consumer gets the same shape for every exception, in every environment. Stack traces stay on the server.
Why I built a Composer package for this
After writing variations of this pattern across three separate projects, I extracted it into satheez/laravel-api-response. The install is one Composer line:
composer require satheez/laravel-api-response
The helper is registered automatically via a service provider. No base controller to extend, no trait to pull in — just call api() from anywhere in the application.
The most useful side effect: code review stops being about response shape. When the shape is settled and comes from one place, reviewers can spend the time on logic instead.