dantepiazza / laravel-api-response
A fluent, expressive API response builder for Laravel 10+
Package info
github.com/dantepiazza/laravel-api-response
pkg:composer/dantepiazza/laravel-api-response
Requires
- php: ^8.3
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0
- phpunit/phpunit: ^10.0|^11.0
This package is auto-updated.
Last update: 2026-05-24 02:39:23 UTC
README
A fluent, expressive JSend-compliant JSON API response builder for Laravel 10+.
Implements the JSend specification: every response carries a
statusofsuccess,fail, orerror, making frontend handling predictable and consistent by convention.
Installation
composer require dantepiazza/laravel-api-response
The service provider and ApiResponse facade are auto-discovered via Laravel's package discovery. No additional setup required.
Publish config (optional)
php artisan vendor:publish --tag=api-response-config
Usage
Three ways to use the package — all share the same singleton instance and are fully interchangeable:
// 1. Helper function api()->success()->response(); // 2. Facade use DantePiazza\LaravelApiResponse\Facades\ApiResponse; ApiResponse::success()->response(); // 3. Dependency injection (recommended for testability) use DantePiazza\LaravelApiResponse\ApiResponse; class UserController extends Controller { public function __construct(private ApiResponse $api) {} public function index(): JsonResponse { return $this->api->success($users, 'Users retrieved')->response(); } }
For a clean DI setup across all controllers, inject in the base Controller:
// app/Http/Controllers/Controller.php abstract class Controller { public function __construct(protected ApiResponse $api) {} }
Response structure
The envelope follows the JSend standard. The code field mirrors the HTTP status code and is always present in the body for clients that don't inspect headers.
success — 2xx
{
"status": "success",
"code": 200,
"message": "Users retrieved",
"data": { ... }
}
data is always present. It will be null if nothing was provided.
message is optional for success and fail.
fail — 4xx
Client-side error. data describes what went wrong (e.g. validation errors).
{
"status": "fail",
"code": 422,
"message": "Validation failed",
"data": {
"email": "The email field is required.",
"password": "Minimum 8 characters."
}
}
error — 5xx
Server-side error. message is required by JSend. data is optional.
{
"status": "error",
"code": 500,
"message": "Internal server error"
}
API Reference
2xx — success
| Method | HTTP | Notes |
|---|---|---|
success(mixed $data = null, string $message) |
200 | Standard success |
created(mixed $data = null, string $message) |
201 | Resource created |
accepted(mixed $data = null, string $message) |
202 | Async / queued |
noContent() |
204 | No body |
4xx — fail
| Method | HTTP | Default message |
|---|---|---|
badRequest(mixed $data = null, string $message) |
400 | The request parameters are incorrect. |
unauthorized(mixed $data = null, string $message) |
401 | Unauthorized |
forbidden(mixed $data = null, string $message) |
403 | Forbidden |
notFound(mixed $data = null, string $message) |
404 | Not found |
methodNotAllowed(mixed $data = null, string $message) |
405 | Method not allowed |
conflict(mixed $data = null, string $message) |
409 | Conflict |
validationError(mixed $data = null, string $message) |
422 | Validation failed |
tooManyRequests(mixed $data = null, string $message) |
429 | Too many requests |
5xx — error
| Method | HTTP | Default message |
|---|---|---|
serverError(mixed $data = null, string $message) |
500 | Internal server error |
serviceUnavailable(mixed $data = null, string $message) |
503 | Service unavailable |
Generic setter
Use when you need full control, e.g. inside a macro:
api()->set(ApiResponse::STATUS_FAIL, 409, 'Already exists', $data); // Status constants: ApiResponse::STATUS_SUCCESS // 'success' ApiResponse::STATUS_FAIL // 'fail' ApiResponse::STATUS_ERROR // 'error'
Data helpers
Merging extra data
api()->success() ->data(['token' => $token, 'expires_in' => 3600]) ->response();
Multiple ->data() calls merge into the same array.
Message-only override
api()->success()->message('Custom message')->response();
Validation errors
Pass errors directly as $data — they land in data per the JSend spec:
api()->validationError('Validation failed', $validator->errors()->toArray())->response();
{
"status": "fail",
"code": 422,
"message": "Validation failed",
"data": {
"email": ["The email field is required."],
"name": ["The name must be at least 3 characters."]
}
}
Pagination
Laravel paginator (automatic)
Pass a LengthAwarePaginator directly — all fields are extracted automatically:
$users = User::paginate(15); return api()->success() ->records($users) ->response();
Manual array / Collection
return api()->success() ->records($users, total: 120, page: 2, pageSize: 15) ->response();
If total is omitted it defaults to count($items).
Pagination output
Both cases produce the same structure:
{
"status": "success",
"code": 200,
"message": "Users retrieved",
"data": {
"results": [...],
"pagination": {
"total": 120,
"per_page": 15,
"current_page": 2,
"last_page": 8,
"from": 16,
"to": 30,
"links": {
"prev": "https://example.com/api/users?page=1",
"next": "https://example.com/api/users?page=3"
}
}
}
}
Cookies
api()->success() ->withCookie(cookie('token', $token, 60)) ->withoutCookie('old_session') ->response();
Configuration
After publishing the config you can rename any top-level JSON key to match an existing API contract:
// config/api-response.php return [ 'keys' => [ 'status' => 'status', // default: 'status' 'code' => 'code', // default: 'code' 'message' => 'message', // default: 'message' 'data' => 'data', // default: 'data' ], ];
Macros
Extend the class with custom shorthand methods in any service provider:
use DantePiazza\LaravelApiResponse\ApiResponse; ApiResponse::macro('teapot', function () { return $this->set(ApiResponse::STATUS_FAIL, 418, "I'm a teapot"); }); // Then anywhere: api()->teapot()->response();
Supported data types
data(), records(), and the $data argument of all shorthand methods accept:
- Plain
array - Any
object(cast via(array)) Illuminate\Support\CollectionIlluminate\Http\Resources\Json\JsonResourceIlluminate\Contracts\Pagination\LengthAwarePaginator(inrecords()only)
Frontend — TypeScript model
The package ships ApiResponse.ts, a single TypeScript class that maps the JSend envelope into a typed object.
import { ApiResponse } from './ApiResponse'; const res = ApiResponse.from(await http.get('/api/users')); if (res.isSuccess()) { console.log(res.data); // unknown | null console.log(res.message); // string // Paginated response const paged = res.records<User>(); if (paged) { paged.records // User[] paged.total // number paged.currentPage // number paged.hasNextPage // boolean } } if (res.isFail()) { // data contains the failure detail (e.g. validation errors) console.log(res.message, res.data); } if (res.isError()) { console.error(res.message); }
Safe factory (returns null instead of throwing on invalid input):
const res = ApiResponse.tryFrom(raw); // ApiResponse | null
Testing
composer install vendor/bin/phpunit
License
MIT