yousef-ahmed-abdalgawad / laravel-api-responder
Unified JSON API responses for Laravel — standardized success, error, and HTTP exception handling
Package info
github.com/yousef2002307/laravel-api-responder
pkg:composer/yousef-ahmed-abdalgawad/laravel-api-responder
Requires
- php: ^8.1
- illuminate/http: ^11|^12|^13
- illuminate/support: ^11|^12|^13
Requires (Dev)
- orchestra/testbench: ^9.0|^10.0
- phpunit/phpunit: ^10.5|^11.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
A lightweight Laravel package that standardizes JSON API responses and handles HTTP exceptions automatically — so every endpoint returns a consistent, predictable structure. Easy to use.
Features
- ✅ Unified JSON response format across all endpoints
- ✅ Trait with helpers for every common HTTP status code
- ✅ Automatic exception handling (401, 403, 404, 405, 422, 429, 500…)
- ✅ Pagination support built-in
- ✅ Laravel auto-discovery — zero manual registration
- ✅ Supports Laravel 11, 12, and 13
- 🔭 Response Observability — automatic
request_id, execution time, endpoint, user ID, environment, and structured error codes injected into every response, with correlated structured logs
Installation
composer require yousef-ahmed-abdalgawad/laravel-api-responder
The package is auto-discovered by Laravel. No need to register the service provider manually.
Usage
1. ApiResponser Trait
Use the trait in any controller to get access to all response helpers:
use YousefAhmedAbdalgawad\ApiResponder\Traits\ApiResponser; class UserController extends Controller { use ApiResponser; public function index() { $users = User::paginate(10); return $this->successResponse( $users->items(), 'Users fetched successfully', 200, [ 'total' => $users->total(), 'per_page' => $users->perPage(), 'current_page' => $users->currentPage(), 'last_page' => $users->lastPage(), ] ); } public function show(User $user) { return $this->successResponse($user, 'User found'); } public function destroy(User $user) { $user->delete(); return $this->successResponseWithoutData('User deleted successfully'); } }
2. ApiExceptionHandler
Register the package's exception handler in your bootstrap/app.php to automatically handle common HTTP exceptions with a consistent JSON format:
use YousefAhmedAbdalgawad\ApiResponder\Exceptions\ApiExceptionHandler; ->withExceptions(function (Exceptions $exceptions): void { ApiExceptionHandler::register($exceptions); })
That's it — all API exceptions will now return structured JSON responses automatically.
Available Trait Methods
Success Responses
| Method | Status | Description |
|---|---|---|
successResponse($data, $message, $statusCode, $pagination) |
200 |
Return data with optional pagination |
successResponseWithoutData($message, $statusCode) |
200 |
Return message only, no data |
Error Responses
| Method | Status | Description |
|---|---|---|
errorResponse($message, $statusCode, $errors, $errorCode, $errorType) |
any | Generic error with optional errors array and structured error code |
unauthorizedResponse($message) |
401 |
Authentication required |
forbiddenResponse($message) |
403 |
Access denied |
notFoundResponse($message) |
404 |
Resource not found |
methodNotAllowedResponse($message) |
405 |
HTTP method not allowed |
conflictResponse($message) |
409 |
Duplicate / conflict |
badRequestResponse($message) |
400 |
Bad request |
requestEntityTooLargeResponse($message) |
413 |
Payload too large |
unsupportedMediaTypeResponse($message) |
415 |
Wrong content type |
serverErrorResponse($message) |
500 |
Internal server error |
serviceUnavailableResponse($message) |
503 |
Service unavailable |
Automatic Exception Handling
When ApiExceptionHandler::register($exceptions) is called, the following exceptions are caught and formatted automatically for API requests (api/* or expectsJson()):
| Exception | Status | Error Code | Message |
|---|---|---|---|
ValidationException |
422 |
VALIDATION_FAILED |
First validation error message |
ThrottleRequestsException |
429 |
RATE_LIMIT_EXCEEDED |
Too many requests + retry_after seconds |
AuthenticationException |
401 |
UNAUTHENTICATED |
Unauthorized |
NotFoundHttpException |
404 |
ROUTE_NOT_FOUND |
Route not found |
MethodNotAllowedHttpException |
405 |
METHOD_NOT_ALLOWED |
Method not allowed |
AccessDeniedHttpException |
403 |
FORBIDDEN |
This action is unauthorized |
ModelNotFoundException |
404 |
RESOURCE_NOT_FOUND |
Resource not found |
QueryException (duplicate) |
409 |
DB_CONSTRAINT_VIOLATION |
Duplicate record |
QueryException (other) |
500 |
DB_QUERY_ERROR |
Database error |
Throwable (fallback) |
500 |
INTERNAL_SERVER_ERROR |
Unexpected error |
Response Format
All responses follow this consistent structure:
Success
{
"status": 200,
"success": true,
"message": "Users fetched successfully",
"data": [...]
}
Error
{
"status": 422,
"success": false,
"message": "The email field is required.",
"errors": {
"email": ["The email field is required."]
}
}
Rate Limited (429)
{
"status": 429,
"success": false,
"message": "Too many requests. Please slow down.",
"retry_after": 45
}
🔭 Response Observability
Response Observability is an opt-in feature that automatically injects a meta block into every API response and a structured error block into every error response. It also emits correlated structured log entries so you can trace any request through your production logs by its request_id.
Setup
1. Publish the config
php artisan vendor:publish --provider="YousefAhmedAbdalgawad\ApiResponder\ApiResponderServiceProvider" --tag=config
2. Enable it in .env
API_OBSERVABILITY_ENABLED=true
3. Register the middleware
Apply the api.observability middleware to your API route group in bootstrap/app.php:
->withMiddleware(function (Middleware $middleware) { $middleware->appendToGroup('api', \YousefAhmedAbdalgawad\ApiResponder\Http\Middleware\ObservabilityMiddleware::class); })
Or scope it to specific route groups in routes/api.php:
Route::middleware(['api.observability'])->group(function () { // your routes });
Observability Response Shape
Success response with observability enabled
{
"status": 200,
"success": true,
"message": "Users fetched successfully",
"data": [...],
"meta": {
"request_id": "req_01J9XKZQP3FGHA8VBRM2NWEY6T",
"timestamp": "2026-08-27T17:30:12Z",
"execution_time_ms": 42.5,
"endpoint": "GET api/users",
"environment": "production"
}
}
Error response with observability enabled
{
"status": 422,
"success": false,
"message": "The email field is required.",
"error": {
"code": "VALIDATION_FAILED",
"type": "validation_error"
},
"meta": {
"request_id": "req_01J9XKZQP3FGHA8VBRM2NWEY6T",
"timestamp": "2026-08-27T17:30:12Z",
"execution_time_ms": 8.1,
"endpoint": "POST api/users",
"http_status": 422,
"exception_class": "Illuminate\\Validation\\ValidationException",
"user_id": 5,
"environment": "production"
}
}
X-Request-ID Header
The ObservabilityMiddleware always sets an X-Request-ID response header — even when observability is disabled. This allows clients, load balancers, and APM tools to correlate requests without needing the full meta block:
X-Request-ID: req_01J9XKZQP3FGHA8VBRM2NWEY6T
Structured Error Codes
Every exception type is mapped to a machine-readable error.code and a human-readable error.type. This makes client-side error handling deterministic — no more parsing error messages.
You can also pass custom codes through the trait's errorResponse() method:
return $this->errorResponse( 'Payment could not be processed.', 402, [], 'PAYMENT_FAILED', 'payment_error' );
Result:
{
"status": 402,
"success": false,
"message": "Payment could not be processed.",
"error": {
"code": "PAYMENT_FAILED",
"type": "payment_error"
}
}
Correlated Log Entries
When log_errors = true (default), every error response is automatically logged with full context:
[2026-08-27 17:30:12] production.ERROR: api.response.error {
"request_id": "req_01J9XKZQP3FGHA8VBRM2NWEY6T",
"method": "POST",
"endpoint": "api/payments",
"http_status": 422,
"execution_time_ms": 8.1,
"user_id": 42,
"ip": "192.168.1.1",
"user_agent": "MyApp/2.0",
"error_code": "VALIDATION_FAILED",
"error_type": "validation_error",
"exception_class": "Illuminate\\Validation\\ValidationException"
}
Now when a user reports an issue, ask them for the X-Request-ID from the response header and grep your logs:
grep "req_01J9XKZQP3FGHA8VBRM2NWEY6T" storage/logs/laravel.log
Config Reference
After publishing, config/api-responder.php exposes the following options:
| Config key | .env variable |
Default | Description |
|---|---|---|---|
observability.enabled |
API_OBSERVABILITY_ENABLED |
false |
Master switch — enable/disable the feature |
observability.log_channel |
API_OBSERVABILITY_LOG_CHANNEL |
null (default channel) |
Which log channel to use |
observability.include_user_id |
API_OBSERVABILITY_USER_ID |
true |
Include authenticated user ID in meta |
observability.include_env |
API_OBSERVABILITY_ENV |
true |
Include app environment in meta |
observability.log_success |
API_OBSERVABILITY_LOG_SUCCESS |
false |
Log 2xx responses (can be noisy) |
observability.log_errors |
API_OBSERVABILITY_LOG_ERRORS |
true |
Log 4xx / 5xx responses |
Overriding the Exception Handler
You can override any specific handler after calling register() — Laravel renders exceptions in registration order:
->withExceptions(function (Exceptions $exceptions): void { // Register package handlers first ApiExceptionHandler::register($exceptions); // Then override specific ones for your app $exceptions->render(function (QueryException $e, $request) { // Your custom logic here }); })
Requirements
- PHP
^8.1 - Laravel
^11 | ^12 | ^13
License
The MIT License (MIT). Please see the LICENSE file for more information.