betalabs/laravel-api-handler
betalabs/laravel-api-handler is a Laravel package for building consistent JSON APIs, centralizing response formatting and exception handling so controllers stay clean and endpoints return predictable, standardized results.
This release is a maintenance-first major: the package is now PHP ^8.2 / Laravel ^10 || ^11 || ^12 only, and the previously silent failure modes (die() on unsupported relations, MySQL HY093 on counted queries) now surface as real errors. If you are on a modern stack, treat v5 as the new baseline; if anything in your app is still on PHP 8.1 or Laravel ≤9, stay pinned to v4.x.
Minimal path in:
composer why-not the package against your app; PHP 8.2+ and Laravel 10+ are hard requirements now that the < 5.8 compatibility branches are gone.config/app.php entry is the most common "it just returns raw Eloquent results" symptom.?_fields=id,name, then ?_with=relation. That progression tells you whether the envelope, the field allowlist and the eager-loading layer are each wired correctly before you combine them._fields/_with, every relation type, fulltext search, counters (including GROUP BY) and envelope cleanup. When a behaviour is ambiguous, the corresponding test is a more reliable spec than the README.First real use case: a paginated index endpoint with a client-controlled _fields and one _with, e.g. GET /api/posts?_fields=id,title&_with=author&page=2. That single request exercises the entire happy path and is the fastest way to confirm your config and the envelope behave.
Keep a per-endpoint allowlist. The package exposes filters, ordering, _fields and _with to the client. Day-to-day, every controller should declare what is permitted rather than relying on defaults — otherwise a caller can sort on an unindexed column, or expand a relation you never intended to expose. Treat the allowed relation list as part of your API contract, because after this release an unsupported relation is a hard 400 rather than a silent no-op.
Compose the query in the controller, not the model. The idiomatic flow is: build the base Builder with your scoping (where('tenant_id', ...), soft-delete awareness, policy checks), then hand it to the handler. Filters applied after the handler runs will silently fight with its own ORDER BY/LIMIT handling.
Lean on _fields and _with for payload shape. _with handles nested relation loading and cleans up afterwards; combined with _fields you can let mobile clients trim responses without a per-client endpoint. Nested paths are supported, so _with=author.profile works — but keep nesting shallow, since each level is a join/eager-load the client can trigger.
Wrap counters deliberately. The count provider is the feature most likely to bite you in production (see below). The safe pattern is to use the envelope's meta counters only on queries you control, and to prefer simplePaginate-style responses for very large or heavily grouped listings.
Centralise the config. Because the Parser now declares $config as a real typed property, the supported extension point for behaviour tweaks is configuration and subclassing, not mutating properties at runtime. Keep your customisation in one config file or one base handler class so future majors touch one file.
Validate before you query. Query-string parameters are user input. Reject unknown filter operators and malformed _fields with a 422 at the edge; the package will happily pass through and you will debug it at the database instead.
Match the package's exception contract. ApiHandlerException is now the package's way of signalling client errors. Make sure your Handler/render (Laravel 11+ bootstrap/app.php or your Exceptions/Handler) renders it as JSON with the intended status — if you have a catch-all that turns everything into a 500, the new 400s will look like server faults in your logs and dashboards.
The _with change is the migration landmine. MorphMany / MorphOne used to hit a die() that emitted HTTP 200 with the literal body Relation type not supported!. It now throws ApiHandlerException → 400. Two consequences: (a) any integration test asserting 200 on a polymorphic _with must be updated, and (b) any client that "worked" because the response was 200 but empty was already broken — surface it now rather than papering over it. If you genuinely need polymorphic expansion, do it explicitly in your controller rather than through _with.
Retest every counted query. CountMetaProvider now clears ORDER BY bindings before counting; previously the count statement sent more bindings than placeholders and MySQL raised HY093. The bug is fixed, but any query where you previously "worked around" it (custom count, manual reorder(), or a hand-rolled meta field) should be re-verified, especially with GROUP BY, where counts are already semantically delicate.
Native fulltext without _fields no longer explodes — but it is still worth specifying. The count(null) TypeError is gone, so ?_fulltext=term on a native search no longer 500s. Don't take that as licence to omit _fields: naming the searched columns keeps results predictable and lets you confirm the index is actually being used with EXPLAIN.
PHP 8.2 dynamic-property deprecation is only fixed for Parser. If you subclass Parser or any other class in the package and assign undeclared properties, you will still get deprecation noise. Declare #[\AllowDynamicProperties] or, preferably, real properties.
Watch the exception handler ordering. A die() removed in favour of an exception can change behaviour under APP_DEBUG=true (stack trace instead of JSON body) and in queued/console contexts. Add a test that asserts the JSON shape and status of a rejected _with so a handler refactor can't silently regress your API contract.
Extension points that survived: config, the handler's field/relation allowlists, and the response envelope. Things that did not: anything relying on Laravel < 5.8 shims, and the old test suite — if you had forked or copied tests from the 4.x repo, they no longer run and have been replaced wholesale.
CI parity is cheap here. The package now runs its suite on GitHub Actions against PHP 8.5/Laravel 12. Mirroring that matrix (at least 8.2 and your production minor) will catch dependency drift before a composer update does it for you.
How can I help you explore Laravel packages today?