Search by

yousef-ahmed-abdalgawad / laravel-api-responder

yousef2002307

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

Statistics

Installs: 5

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

v2.0.0 2026-08-27 15:07 UTC

This package is auto-updated.

Last update: 2026-08-29 10:46:47 UTC


README

Latest Version on Packagist PHP Version Laravel Version License: MIT

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.