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

Api Client Laravel Package

somnambulist/api-client

Somnambulist API Client gives Laravel and PHP developers a structured, testable way to consume external HTTP APIs. Define requests, send them cleanly, handle responses consistently, and keep third-party integrations maintainable without scattering HTTP logic through your app.

View on GitHub
Deep Wiki
Context7

Getting Started

Install the package alongside a concrete PSR-18 HTTP client and PSR-17 factory implementation (the package only defines the abstraction — it does not ship an HTTP transport):

composer require somnambulist/api-client guzzlehttp/guzzle php-http/discovery

Where to look first: open vendor/somnambulist/api-client/src/Client.php. This library is deliberately thin — the Client class is the whole surface area. Read it before writing anything, because it tells you exactly which PSR-18/PSR-17 interfaces you must provide and which convenience methods (if any) it adds on top.

Minimal first use case — fetch a JSON collection from one endpoint:

use GuzzleHttp\Client as HttpClient;
use Somnambulist\Components\ApiClient\Client;

$http = new HttpClient(['base_uri' => 'https://api.example.com']);
$api  = new Client($http);

$response = $api->get('/users', ['page' => 1]);
$users    = json_decode((string) $response->getBody(), true);

For Laravel, bind it once in a service provider so controllers never construct HTTP plumbing:

$this->app->singleton(Client::class, fn () => new Client(
    new HttpClient([
        'base_uri' => config('services.upstream.base_uri'),
        'headers'  => ['Authorization' => 'Bearer '.config('services.upstream.token')],
    ])
));

Implementation Patterns

Wrap the client in a domain-specific API class per upstream service. Don't let Client leak into controllers, jobs, or commands. Give each external API its own class:

final class BillingApi
{
    public function __construct(private Client $client) {}

    public function invoice(int $id): Invoice
    {
        $res = $this->client->get("/invoices/{$id}");
        return Invoice::fromArray(json_decode((string) $res->getBody(), true));
    }
}

This is where the package earns its keep: it standardises how you build a request and return a PSR-7 response, and your wrapper classes add the DTO hydration on top.

Keep authentication and base URI on the underlying PSR-18 client, not on call sites. Guzzle middleware (HandlerStack, Middleware::retry, Middleware::log) and Symfony HttpClient base_uri/defaults give you retry, logging, auth refresh, and rate-limit headers for free — the api-client wrapper does not need to know about them. If you find yourself passing an Authorization header on every call, you've configured the wrong layer.

Config-drive the base URL and version. Point at config('services.upstream.base_uri') so staging/prod swaps and /v2 bumps never appear in code. Centralise this in the singleton binding above.

Test with Psr\Http\Client\ClientInterface mocks. Because the package depends on PSR-18, tests can inject a fake client (Http\Mock\Client, or a small anonymous class returning canned ResponseInterface). This is the single biggest practical advantage of PSR-18 abstraction over instantiating Guzzle directly — lean on it.

DTO hydration pattern. Define fromArray() static constructors on your domain models and keep json_decode there, not in the API wrapper. One wrapper method = one fromArray call, which keeps error handling (missing keys, type coercion) localised.

Idempotency and retries. Retries belong in the underlying handler stack, but idempotency keys should be generated in the wrapper method so they stay stable across retries:

$res = $this->client->post('/charges', [
    'headers'     => ['Idempotency-Key' => (string) Str::uuid()],
    'json'        => $payload,
]);

Gotchas and Tips

Treat this as a reference implementation, not a supported dependency. With 1 star and 0 dependents, assume no security patches, no breaking-change notices, and no issue triage. Either vendor the ~200 lines into your own app/Support/ApiClient folder, or pin it to an exact version and accept you will maintain it. For most teams, a thin wrapper over Guzzle is the lower-risk choice.

You must supply a PSR-18 client. If you composer require somnambulist/api-client alone and try to instantiate Client, you'll get container/type errors. This is by design (HTTP client agnostic) but trips people up. Also note php-http/discovery needs to be present for auto-discovery to work in Laravel's container.

PSR-18 vs PSR-7 confusion. ClientInterface::sendRequest() accepts a RequestInterface and returns a ResponseInterface. If you pass a Guzzle Client where a PSR-18 client is expected, it works (Guzzle 7 implements PSR-18) — but the reverse doesn't. Don't add guzzlehttp/psr7 and assume you're done; you need the client too.

Consume the response body once — and cast it. getBody() returns a StreamInterface. json_decode($response->getBody(), true) fails because json_decode expects a string; always cast with (string) $response->getBody() or use $response->getBody()->getContents() if you haven't read it yet. Calling getContents() twice returns an empty string.

No error handling for 4xx/5xx. PSR-18 clients return responses for all status codes — they do not throw by default. Every wrapper method needs its own status check, or you'll silently treat a 500 HTML error page as valid data. Standardise:

if ($res->getStatusCode() >= 400) {
    throw new UpstreamException($res->getStatusCode(), (string) $res->getBody());
}

Trailing slashes bite. Whether your base URI ends in / and whether you pass /users or users depends on the underlying client's URI resolution. Pick one convention (I recommend base URI without trailing slash, paths always leading with /) and enforce it in the wrapper class, not at call sites.

Extension point. If the package doesn't do something you need (query-string serialisation of nested arrays, response decoding, retry), add it in your own base class or a delegating wrapper — don't fork. The class is small enough that wrapping it once gives you a stable internal API to swap the backing implementation later.

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.
socialdept/atp-orm
cboxdk/laravel-assistant-client
socialdept/atp-resolver
socialdept/tenant-domains
tautvydasr/blade-class-names
socialdept/color-scales
maherelgamil/controller
coleus/widgets
coleus/table
coleus/users
coleus/music
coleus/health
coleus/calendar
coleus/notes
coleus/support
coleus/settings
coleus/apps
seampact/laravel
emmyconceptng/laravel-ng-postcode
jjsquady/round-robin