marcin-orlowski/laravel-api-response-builder
Build consistent, structured JSON API responses in Laravel. Standardize success and error payloads, map custom internal codes to HTTP statuses, and keep every endpoint returning the same predictable, documented format.
Install the package and publish its config:
composer require marcin-orlowski/laravel-api-response-builder
php artisan vendor:publish --provider="MarcinOrlowski\ResponseBuilder\ResponseBuilderServiceProvider"
Laravel auto-discovery registers the service provider and ResponseBuilder facade. Look first at:
config/response_builder.php — response codes, HTTP mappings, debug behavior, encoding.MarcinOrlowski\ResponseBuilder\BaseApiCodes — subclass this for your application error codes.MarcinOrlowski\ResponseBuilder\ResponseBuilder — the facade/builder you will use in controllers.Create one central error-code class early:
namespace App\Support;
use MarcinOrlowski\ResponseBuilder\BaseApiCodes;
class ApiCode extends BaseApiCodes
{
public const USER_NOT_FOUND = 1001;
public const VALIDATION_FAILED = 1002;
public const UNAUTHORIZED = 1003;
}
First use case: return a normalized success response from a controller and a mapped error response when a model is missing:
use App\Support\ApiCode;
use MarcinOrlowski\ResponseBuilder\ResponseBuilder;
public function show(int $id)
{
$user = User::find($id);
if (! $user) {
return ResponseBuilder::asError()
->withCode(ApiCode::USER_NOT_FOUND)
->withHttpCode(404)
->withMessage('User not found')
->build();
}
return ResponseBuilder::asSuccess()
->withData($user)
->build();
}
The payoff is a consistent envelope—typically success, code, message, and data—without hand-building response()->json([...]) in every endpoint.
Convenience helpers are fine for simple cases, but the fluent builder is the day-to-day tool because it makes HTTP status, application code, message, and payload explicit:
return ResponseBuilder::asSuccess()
->withHttpCode(201)
->withMessage('User created')
->withData($user)
->build();
return ResponseBuilder::asError()
->withCode(ApiCode::VALIDATION_FAILED)
->withHttpCode(422)
->withMessage('Validation failed')
->withData(['errors' => $validator->errors()])
->build();
Keep HTTP status and application code deliberately separate. Clients should branch on the application code or success flag, while infrastructure and monitoring can use the HTTP status.
Treat ApiCode constants as a public API contract. Add new codes rather than renumbering existing ones. Map codes to HTTP statuses and human-readable messages in one place—either the package config or a small application service that wraps the builder. Avoid scattering magic integers through controllers.
A good pattern is a thin ApiResponse service or base controller method:
protected function ok(mixed $data = null, string $message = 'OK')
{
return ResponseBuilder::asSuccess()
->withData($data)
->withMessage($message)
->build();
}
protected function fail(int $code, int $httpCode, string $message, mixed $data = null)
{
return ResponseBuilder::asError()
->withCode($code)
->withHttpCode($httpCode)
->withMessage($message)
->withData($data)
->build();
}
This keeps controllers readable and makes future package replacement less painful.
The highest-leverage integration is rendering API exceptions through the same envelope. In app/Exceptions/Handler.php, handle validation, authentication, authorization, and not-found exceptions for JSON requests before delegating to the parent handler:
public function render($request, Throwable $e)
{
if ($request->expectsJson()) {
if ($e instanceof ValidationException) {
return ResponseBuilder::asError()
->withCode(ApiCode::VALIDATION_FAILED)
->withHttpCode(422)
->withMessage('Validation failed')
->withData(['errors' => $e->errors()])
->build();
}
if ($e instanceof AuthenticationException) {
return ResponseBuilder::asError()
->withCode(ApiCode::UNAUTHORIZED)
->withHttpCode(401)
->withMessage('Unauthenticated')
->build();
}
}
return parent::render($request, $e);
}
Be selective: do not convert every exception into a generic API response in production, or you lose framework logging and debugging behavior.
Let services return models, DTOs, arrays, or paginators. Let controllers or a response layer decide how to wrap them. This avoids coupling business logic to the package facade and makes it easier to test services without HTTP concerns.
Do not assume the package will infer your preferred pagination shape. Pick one convention and apply it consistently:
$users = User::paginate();
return ResponseBuilder::asSuccess()
->withData([
'items' => $users->items(),
'meta' => [
'current_page' => $users->currentPage(),
'per_page' => $users->perPage(),
'total' => $users->total(),
],
])
->build();
Document that shape for frontend consumers.
Use Laravel’s JSON assertions to lock down the contract:
$response->assertOk()
->assertJsonPath('success', true)
->assertJsonPath('code', 0)
->assertJsonStructure(['success', 'code', 'message', 'data']);
For errors, assert the application code and HTTP status separately:
$response->assertStatus(404)
->assertJsonPath('success', false)
->assertJsonPath('code', ApiCode::USER_NOT_FOUND);
Avoid asserting exact translated message strings unless localization is part of the test contract.
0 is conventionally the success code; non-zero codes are errors. Do not use 0 for an application error.withHttpCode() explicitly. Otherwise clients may receive a misleading status.config/response_builder.php defines min_code/max_code, codes outside that range may be rejected. Keep application codes inside the configured range.config/response_builder.php, run php artisan config:clear locally. In production, rebuild with php artisan config:cache.APP_DEBUG only in local/staging, and assert debug keys conditionally in tests.JsonResponse, do not pass it through response()->json(...) again.message. Put validation details or safe diagnostic context in data; log the real exception. Avoid exposing raw exception messages to API consumers.BaseApiCodes, the package config, and your own response service. Avoid forking the package for small formatting changes; wrap it instead.How can I help you explore Laravel packages today?