LaravelPackages.net
Acme Inc.
Toggle sidebar
yzen.dev/laravel-json-api-response

Helpers for building JSON:API responses with responder classes

0
0
1.0.0
About yzen.dev/laravel-json-api-response

yzen.dev/laravel-json-api-response is a Laravel package for helpers for building json:api responses with responder classes. It currently has 0 GitHub stars and 0 downloads on Packagist (latest version 1.0.0). Install it with composer require yzen.dev/laravel-json-api-response. Discover more Laravel packages by yzen.dev or browse all Laravel packages to compare alternatives.

Last updated

JsonApiResponse

Packagist Version GitHub Workflow Status Coverage License Packagist Downloads Packagist Downloads PHPStan

JsonApiResponse helps you build JSON:API-compatible response payloads with small responder classes.

The package is framework-light: the responder logic itself works with plain PHP objects and arrays, while HTTP responses and paginated lists use Laravel components:

  • illuminate/http
  • illuminate/pagination

Installation

composer require yzen.dev/json-api-response

Requirements

  • PHP ^8.0
  • illuminate/http
  • illuminate/pagination

Basic Responder

Create a responder by extending AbstractResponder and implementing two methods:

use YzendDev\Laravel\JsonApiResponse\Responder\AbstractResponder;

/**
 * @extends AbstractResponder<User>
 */
final class UserResponder extends AbstractResponder
{
    protected function resourceType(): string
    {
        return 'User';
    }

    protected function composeAttributes(mixed $entity): array
    {
        return [
            'name' => $entity->name,
            'isActive' => $entity->is_active,
            'createdAt' => $entity->created_at->toIso8601String(),
        ];
    }
}

Then compose a resource document:

$response = (new UserResponder())->compose($user);

The resulting payload follows the standard JSON:API shape:

[
    'data' => [
        'id' => 10,
        'type' => 'User',
        'attributes' => [
            'name' => 'yzen.dev',
            'isActive' => true,
            'createdAt' => '2026-05-15T12:00:00+00:00',
        ],
    ],
]

By default, resourceId() returns $entity->id. Override it if your entity uses a different identifier.

Relationships

Define relationships in relationships() and pass the related records through withRelationData().

use YzendDev\Laravel\JsonApiResponse\Responder\RelationshipDefinition;

protected function relationships(): array
{
    return [
        'countryFrom' => new RelationshipDefinition('country_from_id', CountryResponder::class),
        'cityFrom' => new RelationshipDefinition('city_from_id', CityResponder::class),
    ];
}

The relation data must be passed as a map keyed by the foreign key value:

$countries = [
    10 => $country,
    11 => $anotherCountry,
];

$responder = (new UserResponder())
    ->withRelationData('countryFrom', $countries);

If the foreign key is null, or the related object is missing from the map, the relationship value becomes null.

To-Many Relationships

Use ToManyRelationshipDefinition for grouped child resources:

use YzendDev\Laravel\JsonApiResponse\Responder\ToManyRelationshipDefinition;

protected function relationships(): array
{
    return [
        'pickups' => new ToManyRelationshipDefinition('id', PickupResponder::class),
    ];
}

Pass grouped data keyed by the parent entity field declared in entityKey:

$pickups = [
    5 => [
        $pickupA,
        $pickupB,
    ],
    6 => [
        $pickupC,
    ],
];

$responder = (new UserResponder())
    ->withRelationData('pickups', $pickups);

Nested Relationship Data

If a child responder has its own relationships, pass that data through withNestedRelationData():

$responder = (new UserResponder())
    ->withRelationData('pickups', $pickups)
    ->withNestedRelationData('pickups', 'city', [
        7 => $hamburg,
        8 => $berlin,
    ]);

This is useful when a to-many relation needs nested linked resources without loading them inside the responder itself.

Includes

Top-level included resources are defined through includes():

use YzendDev\Laravel\JsonApiResponse\Responder\IncludeDefinition;

protected function includes(): array
{
    return [
        'units' => new IncludeDefinition(UnitResponder::class),
    ];
}

Pass included entities as a flat array:

$responder = (new SomeResponder())->withIncludeData('units', [
    $unitA,
    $unitB,
]);

Paginated Lists

Use composeList() with Laravel's LengthAwarePaginator:

$payload = (new UserResponder())->composeList($paginator);

The package adds pagination metadata automatically:

[
    'meta' => [
        'totalRecords' => 50,
        'perPage' => 15,
        'currentPage' => 2,
        'lastPage' => 4,
    ],
]

HTTP Response Wrapper

YzendDev\Laravel\JsonApiResponse\Responder\JsonResponse extends Laravel's JsonResponse and sets JSON:API headers:

use YzendDev\Laravel\JsonApiResponse\Responder\JsonResponse;

return new JsonResponse($payload);

In a controller, a typical response looks like this:

use YzendDev\Laravel\JsonApiResponse\Responder\JsonResponse;

return new JsonResponse(
    status: JsonResponse::HTTP_OK,
    data: new UserResponder()->compose($request->user()),
);

Headers set by default:

  • Content-type: application/vnd.api+json
  • Charset: utf-8

Error Responses

Validation Errors

use YzendDev\Laravel\JsonApiResponse\Responder\ValidationErrorsResponder;

$errors = (new ValidationErrorsResponder())->compose([
    'email' => ['The email field is required.'],
]);

Ready-to-use JSON:API Error Documents

The package also ships a few ready-made JSON response classes:

  • AuthenticationJsonApiException
  • AuthorizationJsonApiException
  • ResourceConflictJsonApiException
  • ResourceNotFoundJsonApiException
  • ErpSystemJsonApiException

Global Exception Handling

In Laravel, the package works best when JSON:API error rendering is configured once in bootstrap/app.php.

use Illuminate\Auth\Access\AuthorizationException;
use Illuminate\Auth\AuthenticationException;
use Illuminate\Foundation\Application;
use Illuminate\Foundation\Configuration\Exceptions;
use Illuminate\Validation\ValidationException;
use Symfony\Component\HttpKernel\Exception\MethodNotAllowedHttpException;
use Symfony\Component\HttpKernel\Exception\NotFoundHttpException;
use YzendDev\Laravel\JsonApiResponse\Responder\JsonResponse;
use YzendDev\Laravel\JsonApiResponse\Responder\Types\AuthenticationJsonApiException;
use YzendDev\Laravel\JsonApiResponse\Responder\Types\AuthorizationJsonApiException;
use YzendDev\Laravel\JsonApiResponse\Responder\Types\ResourceConflictJsonApiException;
use YzendDev\Laravel\JsonApiResponse\Responder\Types\ResourceNotFoundJsonApiException;
use YzendDev\Laravel\JsonApiResponse\Responder\ValidationErrorsResponder;

return Application::configure(basePath: dirname(__DIR__))
    ->withExceptions(function (Exceptions $exceptions): void {
        $exceptions->render(fn (AuthenticationException $e) => new AuthenticationJsonApiException());
        $exceptions->render(fn (AuthorizationException $e) => new AuthorizationJsonApiException());
        $exceptions->render(fn (AccessDeniedException $e) => new AuthorizationJsonApiException());

        $exceptions->render(function (ValidationException $e) {
            return new JsonResponse(
                status: JsonResponse::HTTP_UNPROCESSABLE_ENTITY,
                data: [
                    'errors' => (new ValidationErrorsResponder())->compose($e->errors()),
                ],
            );
        });

        $exceptions->render(fn (MethodNotAllowedHttpException $e) => new JsonResponse(status: 405));
        $exceptions->render(fn (ResourceNotFoundException $e) => new ResourceNotFoundJsonApiException($e));
        $exceptions->render(fn (NotFoundHttpException $e) => new ResourceNotFoundJsonApiException($e));
        $exceptions->render(fn (ModelNotFoundException $e) => new ResourceNotFoundJsonApiException($e));
        $exceptions->render(fn (FileNotFoundException $e) => new ResourceNotFoundJsonApiException($e));
        $exceptions->render(fn (ResourceConflictException $e) => new ResourceConflictJsonApiException($e));
    })
    ->create();

With this approach, application code can throw domain-level exceptions and let Laravel convert them into JSON:API error documents in one place:

if (! $user) {
    throw new ResourceNotFoundException('User was not found.');
}

That keeps controllers and services focused on business logic while response formatting stays centralized.

Notes

  • Responders are intentionally passive. They format already loaded data and do not fetch dependencies themselves.
  • Relationship maps and grouped child arrays should be prepared before calling the responder.
  • The public usage model in this package follows the examples from responder.md.
Comments