October DealsAmazon USOctober deal check: compare before you payAmazon US: current deals, useful picks and tech finds.Check DealsWindows FixRecommendedWindows errors stealing your time? Find the fix fastScan stability, cleanup and performance issues.Fix NowOctober DealsAmazon USDeal season is back - check today's better picksAmazon US: current deals, useful picks and tech finds.See Picks×
Skip to content
World desk8 min

The Adapter Pattern: A Laravel Developer’s Guide to API Integration

The Adapter pattern puts a provider's authentication, requests, payloads and errors behind an interface your application owns. Here is how to structure it in Laravel, handle non-throwing HTTP responses, and test it.

What’s actually slowing this PC down?

Pick the symptom - the matching free tool is one click away.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The Adapter pattern gives a Laravel application one place where its own vocabulary meets a third-party API. Application code depends on an interface the application owns. A provider-specific adapter handles the provider’s authentication, HTTP requests, payload shape, and failure behaviour. Laravel’s HTTP client does the transport work, but it does not decide how your integration is structured. That decision is yours, and this guide explains how to make it.

What the Adapter pattern is

The Adapter is a structural design pattern. It converts the interface of an existing component into the interface a client expects, so two components that do not match can still work together without changing either one. In the classic version there are three roles:

As an Amazon Associate I earn from qualifying purchases.

  • Target: the interface your application code calls, expressed in your domain terms.
  • Adapter: a class that implements the target and translates each call into whatever the existing component needs.
  • Adaptee: the existing component, here an external API and whatever client code talks to it.

In API integration, the translation is usually broader than renaming methods. The adapter maps application concepts to endpoint paths and request parameters, attaches provider authentication, converts provider-specific response fields into application-facing values, and turns transport and HTTP failures into errors your application understands. The adapter is application architecture. It is not the same thing as Laravel’s built-in HTTP wrapper, which sits underneath it.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

How the layers fit together

A useful way to picture a Laravel integration is as a chain of responsibilities:

Controller or job -> application contract -> provider adapter -> Laravel HTTP client -> external API
  • Controller or job: expresses intent, such as “quote shipping for this parcel.” It never touches a provider’s response array.
  • Application contract: an interface you own, with method names and return types that make sense in your domain.
  • Provider adapter: implements the contract. It holds endpoint paths, authentication, field mapping, and error mapping for one provider.
  • Laravel HTTP client: sends the request, applies timeouts and retries, and returns a response object you inspect.

Credentials belong in configuration or environment secrets, read through config(), not hard-coded in the adapter. Keep the provider’s request details inside the adapter so that a change in the provider’s API touches one file rather than every caller.

What Laravel’s HTTP client gives you

Laravel’s HTTP client is a wrapper around Guzzle with an expressive API for outbound requests. According to the Laravel 13.x HTTP Client documentation, it provides:

  • The Http facade with request methods including get, post, put, patch, and delete.
  • Request configuration for headers, authentication, timeouts, retries, and middleware, plus access to Guzzle options when you need them.
  • Response methods such as status, successful, failed, clientError, serverError, body, and json.
  • Macros, fake responses, and request assertions for testing.

Method signatures and option names change between framework releases, so check the documentation for the version your project runs before copying a signature.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

The error-handling detail you must design for

Laravel’s HTTP Client documentation states that, unlike Guzzle’s default behaviour, the wrapper does not throw exceptions on client or server errors (400- and 500-level responses). A 401, 404, or 500 still returns a response object. If your code only reads json() and continues, a failed call can silently produce empty or wrong data.

The adapter should decide explicitly how each failure type is handled. Connection-level failures are a separate path: they surface as exceptions, so the adapter must catch them as well. The table below shows one common mapping. It is a design choice you should adapt to the provider, not a rule set by Laravel.

Situation What the adapter receives Common application-level outcome
Network failure or timeout A connection exception from the HTTP client A “provider unavailable” error; the caller may retry later
401 or 403 A response with failed() true A configuration or credentials error, usually not retried
404 A response with failed() true A “not found” result for the specific resource
422 or other validation rejection A response whose body explains the rejected fields An invalid-input error carrying the provider’s message
429 or 5xx A response with serverError() or a rate-limit status A temporary failure; a retry candidate for read operations

Retries deserve particular care. Laravel’s retry() method accepts an attempt count, a delay in milliseconds, and an optional condition. Whether a retry is safe depends on the provider operation. Reads are usually safe to repeat. Writes such as creating an order or charging a card are safe to repeat only if the provider guarantees idempotency, typically through an idempotency key it documents. That safety judgment is general engineering practice, not something Laravel’s documentation decides for you.

How to structure a third-party API client

There are two realistic architectures. Choose between them by looking at how much provider detail would leak into your application and how likely it is that the provider, or the set of providers, will change.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Option A: a thin provider-specific client

A focused class wraps the provider’s endpoints with Laravel’s HTTP client and returns simple arrays or small value objects. It is the right choice for a small, stable API with one or two endpoints and little translation. Its weakness is that callers end up depending on provider field names and semantics, so a provider change ripples outward.

Option B: an application-facing contract plus an adapter

The application defines an interface describing what it needs. A provider adapter implements that interface and does all the translation. This is worth the extra class when you have distinct providers, substantial mapping between your model and theirs, or a need to substitute a fake at the application boundary. Laravel’s contracts documentation (Laravel 13.x) notes that contracts are interfaces with framework implementations resolved through the service container, and that the choice between contracts and facades comes down to team preference. Both can produce robust, well-tested applications, and they are not mutually exclusive.

Consideration Thin provider client Contract plus adapter
Provider payloads reach application code Likely, unless callers are careful Kept inside the adapter
Several providers or a realistic chance of switching Each caller would need changes Each provider implements the same contract
Tests need a substitute at the application boundary Often mocked at the HTTP layer only Application tests can fake the contract directly
Maintenance cost Low Higher; the contract must stay aligned with real provider behaviour
Best fit One stable endpoint with little translation Distinct providers or substantial mapping

Swapping providers is rarely effortless even behind a clean contract. Feature coverage, rate limits, authentication models, and data semantics differ, and an interface can hide those differences only up to a point. Expect some application decisions when a second implementation arrives, and do not design the contract as if they will never be needed.

A worked example: a shipping quote adapter

The following example uses a fictional provider, “Acme Shipping,” to show the shape of the code. The contract is owned by the application; the adapter is the only class that knows Acme’s endpoint and field names.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

First, the contract:

<?php

namespace AppShipping;

interface ShippingQuotes
{
    public function quote(Parcel $parcel): ShippingQuote;
}

Next, the adapter. It catches connection failures, maps a 422 to a domain error, and treats any other failed status as a temporary provider problem:

<?php

namespace AppShipping;

use IlluminateHttpClientConnectionException;
use IlluminateSupportFacadesHttp;

final class AcmeShippingAdapter implements ShippingQuotes
{
    public function __construct(
        private string $baseUrl,
        private string $apiKey,
    ) {}

    public function quote(Parcel $parcel): ShippingQuote
    {
        try {
            $response = Http::withToken($this->apiKey)
                ->acceptJson()
                ->timeout(10)
                ->post($this->baseUrl.'/v2/quotes', [
                    'weight_grams' => $parcel->weightGrams,
                    'destination_country' => $parcel->destinationCountry,
                ]);
        } catch (ConnectionException $e) {
            throw new ShippingUnavailable('Shipping provider could not be reached.', previous: $e);
        }

        if ($response->status() === 422) {
            throw new InvalidParcel($response->json('message', 'Parcel rejected.'));
        }

        if ($response->failed()) {
            throw new ShippingUnavailable('Provider returned HTTP '.$response->status().'.');
        }

        return new ShippingQuote(
            amount: $response->json('quote.amount'),
            currency: $response->json('quote.currency'),
        );
    }
}

Bind the contract to the adapter in a service provider so that controllers and jobs type-hint ShippingQuotes:

$this->app->bind(ShippingQuotes::class, function () {
    return new AcmeShippingAdapter(
        config('services.acme.url'),
        config('services.acme.key'),
    );
});

Notice that the adapter returns a ShippingQuote rather than the provider’s array. Everything above that line in the application works with your own type.

Independent reader supportYour contribution helps us test, update, and keep practical guides available for everyone.Support on Ko-Fi

Testing the adapter

Test at two levels. Adapter tests check that the translation is correct in both directions. Application tests check that controllers and jobs behave correctly when the contract returns a quote or throws one of your errors, and they can fake the contract entirely.

Free tools Windows power users keep installed

One-click scans. No signup required.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

For adapter tests, use the HTTP client’s fakes and assertions. The Laravel 13.x HTTP Client documentation describes faking responses, fake sequences, and asserting sent requests. The Laravel 12.x API reference documents the related factory methods fake, fakeSequence, assertSent, and preventStrayRequests. Confirm that these methods exist in the version your project uses before relying on them.

  1. Call Http::preventStrayRequests() in your base test case or in setUp(), so a missing fake fails the test instead of reaching the real API.
  2. Register a fake response for the provider’s URL pattern.
  3. Call the adapter and assert that the returned value is your own type with the expected fields.
  4. Assert on the outgoing request: method, URL, authorisation header, and body.
  5. Repeat with failing responses, such as a 422 and a 503, and assert the mapped exceptions.
use IlluminateHttpClientRequest;
use IlluminateSupportFacadesHttp;

public function test_quote_maps_provider_response(): void
{
    Http::preventStrayRequests();

    Http::fake([
        'api.acme-shipping.example/*' => Http::response([
            'quote' => ['amount' => 1250, 'currency' => 'EUR'],
        ], 200),
    ]);

    $adapter = new AcmeShippingAdapter('https://api.acme-shipping.example', 'test-key');
    $quote = $adapter->quote(new Parcel(weightGrams: 800, destinationCountry: 'DE'));

    $this->assertSame(1250, $quote->amount);

    Http::assertSent(fn (Request $request) =>
        $request->url() === 'https://api.acme-shipping.example/v2/quotes'
        && $request->hasHeader('Authorization', 'Bearer test-key')
    );
}

To test retry behaviour or a sequence of responses, use Http::fakeSequence() and push responses in order, for example a 503 followed by a 200. Test error responses as carefully as successes: the mapping from status to application error is the part most likely to regress silently.

When not to add an abstraction

An interface is a cost, not a default. Skip the contract and keep a focused client when:

  • the integration calls one or two endpoints and the response is used directly in one place;
  • no second provider is planned and the provider is unlikely to change;
  • the tests you need are already well served by faking HTTP responses in the client’s own tests;
  • you would be writing a generic repository or a catch-all “API service” whose methods mirror the provider’s endpoints one-for-one.

Start with the thin client. Extract the contract when a second provider, a substantial translation layer, or an application-level test need actually appears.

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

Product prices and availability are accurate as of the date/time indicated and are subject to change. Any price and availability information displayed on Amazon at the time of purchase will apply.

Leave a Reply

Your email address will not be published. Required fields are marked *

Special offer. See more information about Outbyte and uninstall instructions. Please review EULA and Privacy policy.

More from the Wire

  1. World desk4 min
    How to Spot an AI Voice Scam Before Sending MoneyDon’t rely on how a caller sounds. Pause, call back through a known number, and verify the emergency with another trusted person before sending money.
  2. Mountain View desk4 min
    Google’s SynthID Detector: How to Check AI-Generated Images, Video and AudioGoogle’s SynthID Detector looks for an embedded watermark in supported images, video and audio. Here is what its results do—and do not—show.
  3. Redmond desk20 min
    How to create a link to File or Folder in Windows 11Windows 11 gives you several ways to point to a file or folder without moving or duplicating it. You can create a desktop shortcut,…
Recommended PC Tool
Recommended PC Tool
Crashes, No Sound, or Screen Glitches?Free driver scan
Windows Errors? Fix Them Before They SpreadFree repair scan

Two free Windows tools

One Free Minute Could Fix That PC

Before you go - each of these free tools takes about a minute and tackles what quietly slows a Windows PC down.

Special offer. View Outbyte info, uninstall instructions, EULA, and Privacy Policy.