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

Laravel Transporter Laravel Package

juststeveking/laravel-transporter

Laravel Transporter is a clean, class-based HTTP client for Laravel. Build reusable API clients with configurable base URLs, headers, requests, and responses, keeping external service integrations simple, testable, and maintainable.

View on GitHub
Deep Wiki
Context7

Getting Started

Install and pin the major line:

composer require juststeveking/laravel-transporter:^4.0

Laravel auto-discovery means there is usually no bootstrap step. Publish config only if the package’s defaults need overriding; otherwise leave it alone and configure through your own service/container wiring.

First, read these in order:

  1. The README usage example.
  2. vendor/juststeveking/laravel-transporter/src/Transporter.php
  3. vendor/juststeveking/laravel-transporter/src/Request.php
  4. vendor/juststeveking/laravel-transporter/src/Response.php
  5. vendor/juststeveking/laravel-transporter/src/Pool.php
  6. The package tests — they are the fastest way to confirm the exact method names and payload/response hooks for 4.x.

Your first use case should be deliberately small: replace one read-only Http::get() call in a service with a Transporter/Request pair, then prove it with Http::fake().

A minimal shape looks like this:

namespace App\Integrations\Acme;

use JustSteveKing\Transporter\Transporter;

final class AcmeTransporter extends Transporter
{
    protected string $baseUrl = 'https://api.acme.test/v1';

    protected array $headers = [
        'Accept' => 'application/json',
    ];
}
namespace App\Integrations\Acme\Requests;

use JustSteveKing\Transporter\Request;

final class ListCustomersRequest extends Request
{
    protected string $method = 'GET';

    protected string $path = '/customers';
}
$response = (new AcmeTransporter())->send(
    new ListCustomersRequest(),
);

$payload = $response->json();

For the first endpoint, do not build a full integration layer. Get one request working, fake it in a test, and confirm how the package exposes status codes, JSON, headers, and failures. That knowledge determines whether you need a custom response class, a service wrapper, or both.

Implementation Patterns

One Transporter per vendor, one Request per endpoint. Keep base URL, auth headers, default query parameters, timeouts, and retry policy on the Transporter. Keep method, path, endpoint-specific headers, query data, and payload on the Request. That separation is the main value: changing an API token or base URL should not touch ten endpoint classes.

A practical directory layout:

app/Integrations/Acme/
├── AcmeTransporter.php
├── Requests/
│   ├── ListCustomersRequest.php
│   └── CreateCustomerRequest.php
├── Responses/
│   └── CustomerResponse.php
└── AcmeService.php

Inject the Transporter, not raw HTTP calls. Bind or construct the Transporter through the container so configuration comes from config/services.php / environment variables rather than hardcoded secrets:

final class AcmeTransporter extends Transporter
{
    protected string $baseUrl = 'https://api.acme.test/v1';

    public function __construct()
    {
        parent::__construct();

        $this->headers = [
            'Accept' => 'application/json',
            'Authorization' => 'Bearer ' . config('services.acme.token'),
        ];
    }
}

If the base class supports constructor arguments in your version, prefer passing configuration through them; otherwise set the protected properties in the subclass constructor.

Put domain-shaped responses behind a service or DTO. The package gives you the transport response; your application should not pass $response->json() arrays directly into controllers or jobs. Map once at the boundary:

final class AcmeService
{
    public function __construct(
        private readonly AcmeTransporter $transporter,
    ) {}

    public function customers(): array
    {
        $response = $this->transporter->send(new ListCustomersRequest());

        if ($response->failed()) {
            throw new AcmeException('Unable to list customers');
        }

        return collect($response->json('data'))
            ->map(fn (array $customer) => CustomerData::from($customer))
            ->all();
    }
}

Use a typed DTO or response object for anything that crosses into domain logic. Arrays are convenient until a vendor renames a field and the failure happens in production rather than in CI.

Handle failures explicitly. Decide per integration what a 4xx and 5xx mean. A common service-level pattern is:

$response = $this->transporter->send(new CreateCustomerRequest($payload));

if ($response->failed()) {
    throw new AcmeRequestFailed(
        status: $response->status(),
        body: $response->body(),
    );
}

Do not let vendor error bodies leak into controllers. Translate them into your own exception types or result objects.

Configure retries and timeouts once. Treat retries as a transport policy, not a per-controller behavior. Retry idempotent reads; do not blindly retry POST calls unless the vendor supports idempotency keys. Set timeouts and connect timeouts at the Transporter/HTTP-client layer so every endpoint inherits the same behavior.

Use Pool only for independent requests. Pooling is useful when a page or job needs several unrelated vendor calls — for example, a dashboard aggregating account, invoice, and usage endpoints. Do not pool requests where request B depends on request A’s response. Also verify what the package does when the Pool argument is null: release 4.0.1 made that parameter nullable in ensureRequest, but the release note does not define whether null means “no pooling,” “default pool,” or “lazy unpooled request.” Confirm against source and tests before relying on it for throughput-sensitive code.

Test with Laravel’s HTTP fakes. The package delegates to Laravel’s HTTP client, so your existing test approach still applies:

Http::fake([
    'api.acme.test/v1/customers' => Http::response([
        'data' => [
            ['id' => 1, 'name' => 'Acme'],
        ],
    ]),
]);

$customers = app(AcmeService::class)->customers();

$this->assertSame('Acme', $customers[0]->name);

Http::assertSent(function ($request) {
    return $request->url() === 'https://api.acme.test/v1/customers'
        && $request->method() === 'GET';
});

Use Http::preventStrayRequests() in integration tests so an unexpected real outbound call fails loudly. Test both success and failure paths; response hydration bugs usually hide in error payloads.

Keep the integration replaceable. Wrap the Transporter behind your own service interface when the integration is revenue-critical. Transporters should be thin enough that replacing this package with direct Laravel HTTP-client calls is a contained change, not a rewrite. That matters given the package’s small adoption footprint and single-maintainer risk profile.

Gotchas and Tips

Normalize slashes in base URLs and paths. A base URL ending in / plus a request path starting with / produces // in the final URL. Most APIs tolerate it; some signed/auth’d endpoints do not. Pick one convention and enforce it in review.

Do not put secrets in Request classes. Request classes should describe the endpoint, not own credentials. Auth belongs on the Transporter or a dedicated auth/header provider. This keeps request classes reusable and testable.

The package is not an auth orchestrator. OAuth refresh flows, mTLS, request signing, and multi-tenant token rotation are not solved by adopting Transporters. If your vendor needs those, scope the work separately; you may still use the package for the final HTTP call, but you will be building most of the auth machinery around it.

Verify the 4.0.1 Pool null behavior before using concurrency. The patch is type-widening only, but the runtime semantics matter. Read the implementation and write a test that exercises the null path if you use pooling. Otherwise you may assume you are fanning out concurrently when you are not.

Treat ensureRequest as an internal extension point unless the README documents it. It may be public by visibility but still not a stable contract. Overriding it tightly couples you to package internals; prefer subclassing the documented Transporter/Request/Response surface.

Typed responses are only as good as their validation. If you hydrate a DTO from $response->json(), validate required keys and types. A vendor returning null for a field that your DTO expects as string should fail in a controlled place, not in a random Blade template.

Use Http::fake() deliberately. Fakes match URLs, so keep base URLs consistent between the Transporter and the fake pattern. When debugging a fake that is not matching, dump the actual request with Http::recorded() or assert against $request->url() to see what the package actually sent.

Log correlation IDs, not full payloads. Vendor APIs often return a request ID header. Capture it in your service exception/log context. Avoid logging Authorization headers or customer payloads wholesale; redact before logging.

Publishing config is usually unnecessary. If you do publish it, remember php artisan config:clear after changing environment variables locally or in deployment. Most day-to-day configuration should live in your own config/services.php and Transporter subclasses.

Do not use this for webhook ingestion or OpenAPI-generated clients. It is an outbound request abstraction. Webhooks, event streaming, and schema-generated clients are different problems; mixing them into the Transporter layer makes the integration harder to reason about.

Pin ^4.0 and keep an eye on the 3.x-to-4.x boundary. Upgrading within 4.x is routine. Moving from 3.x to 4.x is a major-version change and should be planned separately from adopting the package for a new integration. Check composer.json for the supported Laravel/PHP matrix before standardizing across services.

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.
htinlynn/genius-helper
maco-studios/hephaestus
elmogy/larafast
xeviant/laravel-paystack
gorilladash/laravel-website-sdk
martialbe/flysystem-oss
chondal/3312client
jeroen-g/flickr
opscale-co/nova-genbi
itsjustvita/laravel-retention
tequilarapido/trackit
gregsphotography/site-builder-filament
cabaoctavio/laravel-query-log
gregsphotography/site-builder-components
io-digital/clickatell
snowsoft/laravel-admin
oxygen/auth
relicsoftware/barakah-payment-gateway
tobya/work-with-composer
equipc/laravel-email-validator