Small, opinionated helpers for building consistent, JSON:API-inspired API resource responses in Laravel:
BuildsApiResource— a trait forJsonResourceclasses that produces a{ id, type, attributes, links }payload, and makesYourResource::collection(...)return an enveloped, paginated collection automatically.ApiResourceCollection— theAnonymousResourceCollectionsubclass behind that: wraps items indataand addslinks/metapagination info for both length-aware and cursor paginators.
- PHP 8.2+
- Laravel 12.x or 13.x
composer require zhylon/api-resourcesThere is nothing to publish or configure — both classes are used directly.
Add the trait to a JsonResource and call toApiResource() from toArray():
use Illuminate\Http\Resources\Json\JsonResource;
use Zhylon\ApiResources\Http\Resources\BuildsApiResource;
class OrderResource extends JsonResource
{
use BuildsApiResource;
public function toArray($request): array
{
return $this->toApiResource(
type: 'order',
attributes: [
'status' => $this->status,
'total' => $this->total,
],
selfHref: route('orders.show', $this->resource),
timestamps: true,
);
}
}produces:
{
"id": "abc123",
"type": "order",
"attributes": {
"status": "paid",
"total": 42.0,
"created_at": "2026-01-01T00:00:00+00:00",
"updated_at": "2026-01-01T00:00:00+00:00"
},
"links": {
"self": { "href": "https://example.test/orders/abc123" }
}
}toApiResource() accepts:
| Parameter | Type | Description |
|---|---|---|
type |
string |
The JSON:API-style resource type, e.g. 'order' |
attributes |
array |
The resource's own attributes |
selfHref |
?string |
Value for links.self.href |
timestamps |
bool |
Appends created_at/updated_at (as ISO-8601 strings) to attributes |
id |
string|int|null |
Overrides automatic id resolution (see below) |
When id isn't passed explicitly, resolveResourceId() resolves it in this order:
safeHashID()on the underlying resource, if it defines one.hashID(), if the underlying resource implementsTobyMaxham\HashId\Interfaces\Hashing.- The resource's primary key (
getKey()), or itsidproperty as a last resort.
None of this requires tobymaxham/laravel-hashid (or any other hashid package) to be installed — the checks use method_exists()/interface_exists(), so they simply no-op down the chain when unavailable.
For full custom control, override resolveResourceId() in your resource class:
use Illuminate\Http\Resources\Json\JsonResource;
use Zhylon\ApiResources\Http\Resources\BuildsApiResource;
class OrderResource extends JsonResource
{
use BuildsApiResource;
protected function resolveResourceId(): string|int|null
{
return $this->resource->uuid;
}
// ...
}Because BuildsApiResource overrides the static collection() method Laravel already gives every JsonResource, you don't need a dedicated collection class — just call it on your resource:
return OrderResource::collection(Order::paginate());This returns an ApiResourceCollection (an AnonymousResourceCollection under the hood) that wraps the items in a data key and — exactly like Laravel's own paginated resource responses — adds links/meta automatically for both Illuminate\Pagination\LengthAwarePaginator and cursor paginators (Illuminate\Contracts\Pagination\CursorPaginator). Passing a plain collection instead of a paginator just returns { "data": [...] }, no extra setup required.
If you need a dedicated collection class (e.g. to add extra top-level meta), extend ApiResourceCollection directly and pass both the resource and the item class it collects into:
use Zhylon\ApiResources\Http\Resources\ApiResourceCollection;
class OrderCollection extends ApiResourceCollection
{
public function __construct($resource)
{
parent::__construct($resource, OrderResource::class);
}
}return new OrderCollection(Order::paginate());composer testThe MIT License (MIT). See LICENSE for more information.