Weave Code
Weaver
Helps Laravel developers discover, compare, and choose open-source packages. See popularity, security, maintainers, and scores at a glance to make better decisions.
Feedback
Share your thoughts, report bugs, or suggest improvements.
Subject
Message

Json Api Client Laravel Package

enm/json-api-client

Abstract client-side PHP implementation of the JSON:API specification. Build requests with JsonApiClient using any PSR-18 HTTP client, execute them, and deserialize responses into documents and included resources via the bundled serializer and deserializer.

View on GitHub
Deep Wiki
Context7

Getting Started

enm/json-api-client is the transport layer of the ENM JSON:API stack: it builds JSON:API request objects (includes, sparse fieldsets, sort, pagination, filters), sends them over any PSR-18 HTTP client, and deserializes the response into typed Document / Resource objects from enm/json-api-common. It is deliberately abstract — it knows the JSON:API format, not your domain. The mapping to Eloquent models, DTOs, auth headers, retries and pagination loops are all yours to write.

Note before you commit: the repo is archived (last release 2019-08-29) and has no dependents. Treat adoption as "vendor with intent to fork" rather than "install and forget" — see Gotchas.

1. Install the client plus a PSR-18/PSR-17 stack.

composer require enm/json-api-client
# Laravel already ships Guzzle 7, which is PSR-18, and GuzzleHttp\Psr7\HttpFactory
# implements all three PSR-17 factories — no extra deps needed.

The README suggests kriswallsmith/buzz + nyholm/psr7; inside Laravel you can reuse Guzzle instead and avoid a second HTTP stack.

2. Read the enm/json-api-common docs first. Everything you will actually touch — ResourceInterface, RelationshipInterface, DocumentInterface, ErrorInterface, LinksInterface — is defined there, not in this package. The client README is short because the model lives upstream.

3. Wire it once as a container singleton. The constructor takes the base URI, an HTTP client, three factories and a serializer/deserializer pair — too much ceremony to build per call site:

// app/Providers/JsonApiServiceProvider.php
$this->app->singleton(JsonApiClient::class, function () {
    $baseUrl = rtrim(config('services.jsonapi.url'), '/');
    $http    = new \GuzzleHttp\Client(['timeout' => 10]);
    $factory = new \GuzzleHttp\Psr7\HttpFactory();

    return new JsonApiClient(
        $baseUrl,
        $http,          // Psr\Http\Client\ClientInterface
        $factory,       // UriFactoryInterface
        $factory,       // RequestFactoryInterface
        $factory,       // StreamFactoryInterface
        new Serializer(),   // Enm serializer, see package README
        new Deserializer()
    );
});

4. First use case — read one resource. Pass a relative URI; the client prefixes the base URI (README example: http://example.com/api + /myResources/1).

use Nyholm\Psr7\Uri;

$request = $client->createGetRequest(new Uri('/articles/1'));
$request->requestInclude('author', 'comments');

/** @var \Enm\JsonApi\Model\Document\ResourceDocumentInterface $document */
$document = $client->execute($request);
$article  = $document->data();                       // ResourceInterface

$article->id();                                      // string
$article->attributes()->get('title');
$article->relationships()->get('author')->resource()->attributes()->get('name');

If that round-trip works, everything else is composition.

Implementation Patterns

Repository wrapper per resource type. Never let JsonApiClient leak into controllers. One thin class per upstream resource keeps request-building in one place and gives you a seam for caching and tests:

final class ArticleRepository
{
    public function __construct(private JsonApiClient $client) {}

    public function find(string $id): ?ResourceInterface
    {
        $request = $this->client->createGetRequest(new Uri("/articles/{$id}"));
        $request->requestInclude('author');

        $document = $this->client->execute($request);

        return $document->data();
    }
}

Query building is fluent per request. requestInclude(), requestFieldset($type, [...]), requestSort([...]), requestPagination([...]), requestFilter([...]) are all setters on the request object, so compose them conditionally from your own query DTO:

$request = $client->createGetRequest(new Uri('/articles'));
$request->requestSort(['-publishedAt']);
$request->requestFieldset('articles', ['title', 'publishedAt']); // sparse fieldset
$request->requestPagination(['number' => $page, 'size' => 25]);
$request->requestFilter(['status' => 'published']);

The client only serializes these as query params — whether the server honours them is the server's contract. Verify against the API before relying on filters for correctness.

Write path: build a Resource, attach it to the request. POST and PATCH both take a resource body; PATCH should carry only the id plus the changed attributes (JSON:API merge semantics):

$resource = new Resource('articles');
$resource->attributes()->set('title', $data['title']);
$resource->relationships()->set('author', new Relationship(new Resource('people', $authorId)));

$request = $client->createPostRequest(new Uri('/articles'));
$request->addResource($resource);

$created = $client->execute($request)->data();  // server-assigned id

DELETE returns an empty document (often 204) — do not read data() without a null/empty guard.

Hydrate to domain objects in one place. The deserializer gives you generic Resource instances with string ids. Put the mapping in a dedicated hydrator (attribute('publishedAt') → CarbonImmutable, id() → int) rather than sprinkling casts across repositories; JSON:API ids are always strings and will silently break find() calls if you forget.

Pagination by following document links. The client does not auto-page. Read the document's links() collection, pull the next/prev href, turn it into a Uri, and loop — or expose links() to your front end so the UI can drive paging.

Map errors into Laravel-land. JSON:API failures arrive as an ErrorDocument (or a RequestException for non-2xx, depending on the path taken in execute()). Handle both, and translate errors()->… entries into ValidationException when source.pointer points at an attribute — that gets you free 422s in Laravel.

Test by faking the PSR-18 client, not the network. A stub ClientInterface returning a canned JSON:API payload exercises the serializer, deserializer and your hydrator in one shot — far more valuable than asserting on a JSON string. Http::fake() does not apply here; keep the fake at the PSR-18 boundary.

Gotchas and Tips

  • Archived in 2019 — audit before adopting. PHP constraint is pinned to the 7.x era; on PHP 8.x expect deprecation noise and possible type errors. Run the full read/write matrix in a spike branch first, and plan to fork (composer require from a VCS repo) if you hit anything.
  • The base URI is a prefix, not a resolver. new Uri('/articles/1') becomes {baseUri}/articles/1. Watch trailing vs leading slashes; a base URL of /api/ plus /articles can produce /api//articles. Normalize config('services.jsonapi.url') once.
  • No auth, no retries, no rate limiting. These belong in the PSR-18 layer: decorate the Guzzle client with middleware (or a token-injecting decorator) so the token is attached to every outgoing request, including retries. Do not try to thread headers through each request object.
  • Debug by logging the PSR-18 traffic, not the request object. The PSR-7 request is built inside execute(), so dd($request) shows you query params but not the wire format. Wrap the HTTP client and log method/URI/headers/body plus the raw response — that catches wrong Accept headers and malformed bodies immediately.
  • included is only populated for requested includes. $document->included() is empty unless you called requestInclude(); relationships degrade to linkage-only identifier objects. Request all needed includes in the first call to avoid N+1 at the API boundary.
  • Everything is a string and nullable. id(), attribute(), meta() values may be null; data() may be null for 200 responses with data: null. Guard before hydrating.
  • Extension points are the serializer/deserializer and the client itself. Swap Deserializer if you need custom Resource subclasses in your domain; extend JsonApiClient if you want per-call defaults (e.g. always request a fieldset) rather than repeating them in every repository.
  • requestFieldset silently changes what exists. Missing attributes after adding a sparse fieldset are a serialization win, not a bug — but code that assumed attributes()->get('body') will start returning null. Keep fieldsets in the repository, not in callers.
Weaver

How can I help you explore Laravel packages today?

Conversation history is not saved when not logged in.
Prompt
Add packages to context
No packages found.
3mdikram/worldmoneywords
snowsoft/config
itmosoft/laravel-subscriptions
larva/laravel-ai-qwen
diephp/sequences
users-au/socialite-provider
twidpay/twid-logger
mrnewport/laravel-plaid
mrnewport/laravel-repo
mrnewport/laravel-docsign
mrnewport/laravel-stow
mrnewport/laravel-flow
mrnewport/laravel-priceable
mrnewport/laravel-nestedterms
fredmeng/laravel-apidoc-generator
itstructure/laravel-multilingual-tools
pdustdar/elasticsearch
markupai/php-sdk
rlacerda83/lumem-microservice-shipment
uni-method/symfony-lib