What’s actually slowing this PC down?
Pick the symptom - the matching free tool is one click away.
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.
How the layers fit together
A useful way to picture a Laravel integration is as a chain of responsibilities:
#1 Best Overall
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
Httpfacade with request methods includingget,post,put,patch, anddelete. - 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, andjson. - 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.
Quick wins for a faster PC:
Scan for outdated or missing drivers - takes under a minuteDriver Scan →Clear out junk files and repair common Windows errorsFree Scan →Fix the driver behind crashes, sound loss and screen glitchesFind Drivers →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.
Outdated Drivers Are Slowing You Down
One free scan finds every outdated or missing driver and matches the right update for your exact hardware.Free scan · exact hardware matchPC Slower Than It Used to Be?
A free scan shows the junk files, broken settings and background clutter dragging Windows down - then fixes them in one click.Free scan · Windows 10 & 11Option 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.
Rank #3
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.
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:
Rank #4
<?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.
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.
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.
Best Value
- Call
Http::preventStrayRequests()in your base test case or insetUp(), so a missing fake fails the test instead of reaching the real API. - Register a fake response for the provider’s URL pattern.
- Call the adapter and assert that the returned value is your own type with the expected fields.
- Assert on the outgoing request: method, URL, authorisation header, and body.
- 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.
Recommended Free Tools
Quick Recap
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.




