PHPackages                             imran/laravel-api-toolkit - PHPackages - PHPackages  [Skip to content](#main-content)[PHPackages](/)[Directory](/)[Categories](/categories)[Trending](/trending)[Leaderboard](/leaderboard)[Changelog](/changelog)[Analyze](/analyze)[Collections](/collections)[Log in](/login)[Sign up](/register)

1. [Directory](/)
2. /
3. [HTTP &amp; Networking](/categories/http)
4. /
5. imran/laravel-api-toolkit

ActiveLibrary[HTTP &amp; Networking](/categories/http)

imran/laravel-api-toolkit
=========================

Standardized JSON API responses and an automatic, environment-aware exception handler for Laravel APIs.

v1.1.0(1mo ago)00MITPHPPHP ^8.1|^8.2|^8.3|^8.4

Since Jul 18Pushed 1mo agoCompare

[ Source](https://github.com/grim-reapper/laravel-api-toolkit)[ Packagist](https://packagist.org/packages/imran/laravel-api-toolkit)[ Docs](https://github.com/grim-reapper/laravel-api-toolkit)[ RSS](/packages/imran-laravel-api-toolkit/feed)WikiDiscussions master Synced 2w ago

READMEChangelog (1)Dependencies (10)Versions (2)Used By (0)

Laravel API Toolkit
===================

[](#laravel-api-toolkit)

Standardized JSON API responses and an automatic, environment-aware exception handler for Laravel APIs — built for teams shipping a REST/JSON API to a frontend (Next.js, Vue, mobile, etc.) that needs one predictable response shape, always.

- A clean `ApiResponse` facade/trait: `ApiResponse::success($data)`, `ApiResponse::error($message, $code)`, `ApiResponse::loading()`, and more.
- Zero-config exception normalization: validation errors, `ModelNotFound`, auth/authorization failures, 404/405/429, and uncaught 500s are all automatically converted into the same JSON envelope — no changes to your `Handler.php` or `bootstrap/app.php` required.
- Validation errors are flattened into a frontend-friendly `field`/`message` array by default (configurable back to Laravel's native nested shape).
- Environment-aware: stack traces and exception details are only ever included when `APP_DEBUG` (or the package's own debug flag) is on.
- A typed `ApiException` you can throw directly from domain/business code (`throw ApiException::notFound('Order not found.')`) with its own status/error\_code/structured errors/headers.
- Paginators passed into `success()` are automatically unwrapped into `data` + `meta.pagination` — no manual reshaping needed.
- Custom/domain exceptions mapped via `exception_map` can carry their own structured `errors` array, and HTTP headers an exception carries (`Retry-After`, `Allow`, ...) are preserved onto the normalized response.

Installation
------------

[](#installation)

```
composer require imran/laravel-api-toolkit
```

The service provider and `ApiResponse` facade are auto-discovered. Publish the config (and optionally the language file) if you want to customize anything:

```
php artisan vendor:publish --tag=api-toolkit-config
php artisan vendor:publish --tag=api-toolkit-lang
```

Response contracts
------------------

[](#response-contracts)

**Success**

```
{
    "success": true,
    "message": "Request was successful.",
    "data": { "id": 1, "name": "Jane Doe" },
    "meta": { "total": 1 }
}
```

`meta` is omitted entirely when empty.

**Error**

```
{
    "success": false,
    "message": "The given data was invalid.",
    "error_code": "VALIDATION_ERROR",
    "errors": [
        { "field": "email", "message": "The email field is required." }
    ]
}
```

`errors` is omitted when not applicable. With `api-toolkit.debug` enabled, error responses also include a `debug` key (`exception`, `file`, `line`, and `trace` if `show_trace` is on) — but only for genuinely uncaught/unmapped exceptions (the 500 fallback). A mapped/known exception (validation, not-found, your own `exception_map`/`ApiException` entries, ...) never gets a `debug` key regardless of `debug` mode, since its status/message/error\_code are already accurate and self-explanatory — `debug` exists to help diagnose the unexpected, not to duplicate what's already in the response.

**Loading / processing** (HTTP 202 — for async jobs / polling endpoints)

```
{
    "success": true,
    "status": "processing",
    "message": "Your request is being processed.",
    "data": { "job_id": "abc-123" }
}
```

**Paginated success** — pass a paginator straight into `success()`:

```
return ApiResponse::success(User::paginate(15));
// or with a resource: return ApiResponse::success(UserResource::collection(User::paginate(15)));
```

```
{
    "success": true,
    "message": "Request was successful.",
    "data": [ { "id": 1 }, { "id": 2 } ],
    "meta": {
        "pagination": {
            "current_page": 1,
            "per_page": 15,
            "total": 42,
            "last_page": 3,
            "from": 1,
            "to": 15,
            "path": "https://example.com/api/users",
            "nav": { "first": "...?page=1", "last": "...?page=3", "prev": null, "next": "...?page=2" }
        }
    }
}
```

Works with `LengthAwarePaginator` (`paginate()`), the simple `Paginator` (`simplePaginate()` — same shape minus `total`/`last_page`/`nav.last`), and `CursorPaginator` (`cursorPaginate()` — `next_cursor`/`prev_cursor` instead of page numbers), whether passed raw or wrapped in a Resource collection. Set `api-toolkit.paginate` to `false` to disable and pass paginators through untouched.

Usage
-----

[](#usage)

### Facade

[](#facade)

```
use Imran\ApiToolkit\Facades\ApiResponse;

return ApiResponse::success($user);
return ApiResponse::success($user, 'User fetched.', 200, ['total' => 1]);
return ApiResponse::created($user);
return ApiResponse::deleted();
return ApiResponse::noContent();
return ApiResponse::loading('Export queued.', ['job_id' => $job->id]);

return ApiResponse::error('Something went wrong.', 400);
return ApiResponse::unauthorized();
return ApiResponse::forbidden();
return ApiResponse::notFound('User not found.');
return ApiResponse::validationError($validator->errors());
```

### Controller trait

[](#controller-trait)

Prefer calling these methods directly on the controller instead of importing the facade everywhere:

```
use Imran\ApiToolkit\Traits\ApiResponser;

class UserController extends Controller
{
    use ApiResponser;

    public function show(User $user)
    {
        return $this->success($user);
    }

    public function store(StoreUserRequest $request)
    {
        $user = User::create($request->validated());

        return $this->created($user);
    }
}
```

The trait uses common names (`success`, `error`, `created`, `deleted`, `forbidden`, `notFound`, ...). If a base controller you're adopting this into already defines one of these itself, PHP will raise a "trait method has not been applied, because there are collisions" fatal error. Alias around it:

```
class Controller extends BaseController
{
    use ApiResponser {
        success as protected apiSuccess;
        error as protected apiError;
    }

    // your own success()/error() methods keep working; call apiSuccess()/apiError() for this package's versions
}
```

### Global helpers

[](#global-helpers)

```
return api_success($user);
return api_error('Invalid request.', 400);
```

### Throwing typed API errors: `ApiException`

[](#throwing-typed-api-errors-apiexception)

For business/domain errors, throw `ApiException` directly instead of round-tripping through `exception_map` config:

```
use Imran\ApiToolkit\Exceptions\ApiException;

throw ApiException::notFound('Order not found.');
throw ApiException::forbidden();
throw ApiException::conflict('Email already registered.');
throw ApiException::validation(['email' => ['Already taken.']]);
throw ApiException::tooManyRequests('Slow down.', retryAfterSeconds: 30);
throw ApiException::badRequest('Malformed payload.');

// Or fully custom:
throw ApiException::make('Upstream provider timed out.', 502, 'UPSTREAM_TIMEOUT');
```

It self-renders into the standard JSON envelope even if `auto_register` is disabled (Laravel calls an exception's own `render()` method when it defines one — this runs *before* the package's `exception_map`/built-in mapping, so `ApiException` always wins regardless of that config). Because of this, `ApiException` always responds with JSON, even for a request that didn't ask for it — only throw it from API code paths.

By default, `report()` suppresses Laravel's normal exception logging for expected client errors (status &lt; 500) — the same way Laravel itself already treats `ValidationException`/`ModelNotFoundException` — while genuine 5xx errors are still logged as usual. Extend `ApiException` and override `report()` if you want different behavior.

Automatic exception handling
----------------------------

[](#automatic-exception-handling)

The moment the package is installed, any exception thrown while handling a request that expects JSON (or matches `config('api-toolkit.paths')`, `api/*` by default) is normalized automatically:

ExceptionStatus`error_code``ValidationException`422`VALIDATION_ERROR``AuthenticationException`401`UNAUTHENTICATED``AuthorizationException` / `AccessDeniedHttpException`403`FORBIDDEN``ModelNotFoundException` / `NotFoundHttpException`404`NOT_FOUND``MethodNotAllowedHttpException`405`METHOD_NOT_ALLOWED``ThrottleRequestsException`429`TOO_MANY_REQUESTS`Any other `HttpExceptionInterface`its own statuslooked up from `error_codes`Anything else (uncaught 500s)500`SERVER_ERROR`Requests that don't expect JSON and don't match `paths` are left completely untouched — your normal Blade error pages keep working.

### Adding your own exceptions

[](#adding-your-own-exceptions)

Map any exception (including your own domain exceptions) in `config/api-toolkit.php` without writing any handler code:

```
'exception_map' => [
    \App\Exceptions\InsufficientBalanceException::class => [
        'status' => 422,
        'error_code' => 'INSUFFICIENT_BALANCE',
        'message' => 'The account balance is too low for this operation.',
    ],
],
```

If a mapped (or built-in) exception defines its own public `errors()` method — the same convention `ValidationException` and `Illuminate\Contracts\Validation\Validator` already use — its return value is used as the response's `errors` array automatically:

```
class InsufficientBalanceException extends \Exception
{
    public function errors(): array
    {
        return ['balance' => ['The account balance is too low for this operation.']];
    }
}
```

HTTP headers the exception itself carries are also preserved onto the normalized response — e.g. `Retry-After` on a throttling exception or `Allow` on a method-not-allowed exception — matching Laravel's own default behavior.

### Disabling auto-registration

[](#disabling-auto-registration)

If you'd rather wire it up yourself (e.g. you already have a heavily customized `Handler.php`), set:

```
// config/api-toolkit.php
'auto_register' => false,
```

and call the renderer manually from your own exception handler:

```
use Imran\ApiToolkit\Exceptions\ExceptionRenderer;

$renderer = app(ExceptionRenderer::class);

if ($renderer->shouldHandle($request)) {
    return $renderer->render($e, $request);
}
```

Configuration reference (`config/api-toolkit.php`)
--------------------------------------------------

[](#configuration-reference-configapi-toolkitphp)

KeyDefaultDescription`auto_register``true`Automatically hook into Laravel's exception handler.`paths``['api/*']`Path patterns always treated as API requests.`debug``env('API_TOOLKIT_DEBUG', env('APP_DEBUG'))`Include exception details on 500s.`show_trace``false`Include the full stack trace when `debug` is on.`errors_format``flat``flat` (array of `{field, message}`) or `nested` (Laravel default).`paginate``true`Auto-unwrap paginators passed to `success()` into `data` + `meta.pagination`.`log_unhandled``false`Log uncaught/unmapped exceptions through this package too. Off by default — Laravel already reports every exception through its own logging independently of this package, so leaving this on would double every log entry. Only turn it on if you've suppressed Laravel's own reporting for these exceptions.`log_channel``null`Log channel to use when `log_unhandled` is on (`null` = default).`messages`see configFallback messages, also translatable via `resources/lang/{locale}/api-toolkit.php`.`error_codes`see configHTTP status → machine-readable error code.`status_messages`see configHTTP status → which `messages` key to use as the default message. Statuses not listed fall back to the status's own standard HTTP reason phrase, so a response is never shipped with an empty message.`exception_map`see configException class → status/error\_code/message.### Message resolution order

[](#message-resolution-order)

For any error response, the message shown is resolved in this order: (1) an explicit `$message` argument, (2) an `exception_map` override, (3) the exception's own non-empty message, (4) the `status_messages` → `messages` lookup for that HTTP status, (5) the status's standard HTTP reason phrase (e.g. `Unprocessable Entity`) as the final fallback. A response's `message` is never empty.

Testing
-------

[](#testing)

```
composer install
composer test
```

License
-------

[](#license)

MIT.

###  Health Score

39

—

LowBetter than 84% of packages

Maintenance90

Actively maintained with recent releases

Popularity0

Limited adoption so far

Community6

Small or concentrated contributor base

Maturity51

Maturing project, gaining track record

 Bus Factor1

Top contributor holds 100% of commits — single point of failure

How is this calculated?**Maintenance (25%)** — Last commit recency, latest release date, and issue-to-star ratio. Uses a 2-year decay window.

**Popularity (30%)** — Total and monthly downloads, GitHub stars, and forks. Logarithmic scaling prevents top-heavy scores.

**Community (15%)** — Contributors, dependents, forks, watchers, and maintainers. Measures real ecosystem engagement.

**Maturity (30%)** — Project age, version count, PHP version support, and release stability.

###  Release Activity

Cadence

Unknown

Total

1

Last Release

45d ago

### Community

Maintainers

![](https://www.gravatar.com/avatar/7252105628ffa6f064585370161626c7c72e68dd2e74ee66aa50d5894543c183?d=identicon)[webz2feel](/maintainers/webz2feel)

---

Top Contributors

[![grim-reapper](https://avatars.githubusercontent.com/u/7957389?v=4)](https://github.com/grim-reapper "grim-reapper (2 commits)")

---

Tags

responsejsonapilaravelvalidationrestexception handler

###  Code Quality

TestsPHPUnit

### Embed Badge

![Health badge](/badges/imran-laravel-api-toolkit/health.svg)

```
[![Health](https://phpackages.com/badges/imran-laravel-api-toolkit/health.svg)](https://phpackages.com/packages/imran-laravel-api-toolkit)
```

###  Alternatives

[psalm/plugin-laravel

Psalm plugin for Laravel

3365.5M359](/packages/psalm-plugin-laravel)[api-platform/laravel

API Platform support for Laravel

58190.1k22](/packages/api-platform-laravel)[laravel/mcp

Rapidly build MCP servers for your Laravel applications.

80732.6M276](/packages/laravel-mcp)[aedart/athenaeum

Athenaeum is a mono repository; a collection of various PHP packages

255.2k](/packages/aedart-athenaeum)[laravel/pulse

Laravel Pulse is a real-time application performance monitoring tool and dashboard for your Laravel application.

1.7k17.6M172](/packages/laravel-pulse)[laravel/scout

Laravel Scout provides a driver based solution to searching your Eloquent models.

1.7k59.5M721](/packages/laravel-scout)

PHPackages © 2026

[Directory](/)[Categories](/categories)[Trending](/trending)[Changelog](/changelog)[Analyze](/analyze)
