Fastrack your API integrations with the connector pattern
Integrating with external APIs is a common task for a developer, but it can quickly become a hard to maintain mess! I’ve often found myself repeating the same boilerplate code across different projects—manually setting up headers, handling authentication, and formatting responses for every single endpoint.
When working on complex applications, consistency is king. That’s why I've built motomedialab/connector. It’s a super lightweight, opinionated package for Laravel designed to clean up your API integrations by using a dedicated "Connector" and "Request" pattern.
But this isn't the first package to do this?
I hear you ask... and you'd be right, there are packages such as Saloon but from my experience with them, they're often too heavy and too opinionated to pick up and run.
The Problem: API Boilerplate
Typically, you'd use Laravel’s HTTP client directly or a service class written for each integration. This works great for a one-off call, but as soon as you have multiple endpoints for the same service, you’re stuck repeating the base URL and authentication logic.
If you’ve read my previous posts on the Action Pattern or using interfaces, you might have guessed that I’m a big fan of abstracting logic to keep things maintainable. The Connector pattern takes this a step further for your outbound requests.
The Solution: Connectors and Requests
This package splits your API logic into two core components:
-
The Connector: This defines where you are going and how you are authorising these requests.
-
The Request: This defines what you are requesting, and the format for the response you are expecting.
Getting Started
Like all great plugins, you can pull the package in via Composer:
composer require motomedialab/connector
Defining your first connector
First, you'll create a connector class that extends Motomedialab\Connector\BaseConnector. This is where you'll house the third party API URL and any authentication logic.
For this particular example, we'll communicate with GitHub's API.
namespace App\Integrations\Connectors;
use Motomedialab\Connector\BaseConnector;
use Illuminate\Http\Client\PendingRequest;
class GithubConnector extends BaseConnector
{
public function apiUrl(): string
{
return 'https://api.github.com/';
}
public function authenticateRequest(PendingRequest $request): PendingRequest
{
return $request->withToken(config('services.github.token'));
}
}
Note that you're not forced with your authentication to follow a specific procedure, you're in control and you have the flexibility to manage this yourself!
And now for the Request
Next, we'll define our specific request. By extending Motomedialab\Connector\BaseRequest, we only need to specify a couple of values specific to this API call.
namespace App\Integrations\Requests\Github;
use Motomedialab\Connector\BaseRequest;
use Motomedialab\Connector\Contracts\RequestInterface;
use Illuminate\Http\Client\Response;
/**
* @implements RequestInterface<array>
*/
readonly class GetUserRequest extends BaseRequest implements RequestInterface
{
public function __construct(protected string $username)
{
// we'll pass in the username
}
public function endpoint(): string
{
return "users/{$this->username}";
}
public function toResponse(Response $response): array
{
return $response->json();
}
}
Again, note how you have full control here. You can pass whatever arguments you want in, and you can handle the response in any way you like, want to return a Data Transfer Object (DTO)? No problem, be my guest!
You'll also see the small amount of Docblock at the top - this is there to tell your IDE exactly what the endpoint will be returning.
Putting it all together
Now that you've built out your first connector and request, it's as simple as calling it...
use App\Integrations\Connectors\GithubConnector;
use App\Integrations\Requests\Github;
$connector = new GithubConnector();
$userData = $connector->send(new Github\GetUserRequest('chrispage1'));
var_dump($userData); // and here will be your JSON payload!
And that really is all there is to it. Now imagine, you want to integrate with another GitHub endpoint. All you'd need to do is copy your GetUserRequest, update the endpoint() URL and you're ready to go!
Why use this pattern?
- Consistency: Every API integration in your project can follow the same structure. No more guessing where this logic is hidden. I'd recommend storing these in a structured namespace such as
App\Integrations. - Testability: Because your requests are standalone classes, they couldn't be easier to mock and unit test.
- Readability: Your controllers can stay focused on business logic, rather than HTTP configurations.
- Response Transformation: The
toResponse()method allows you to format the data (or handle the errors) exactly how your application needs, before it even hits your service layer.
A quick example on response transformation...
By modifying the what the toResponse method returns, we can easily create DTOs or handle errors. In this instance, we'll create a request to fetch a collection of user repositories
/**
* @implements RequestInterface<Collection<int,GithubRepository>|ApiError>
*/
readonly class ListUserRepositories extends BaseRequest implements RequestInterface
{
public function __construct(public string $user, public int $page = 1)
{
//
}
public function endpoint(): string
{
return 'users/' . $this->user . '/repos';
}
public function queryParams(): array
{
return [
'per_page' => 5,
'page' => $this->page,
];
}
public function toResponse(Response $response): Collection|ApiError
{
return when(
$response->ok(),
fn(): Collection => collect($response->json())->mapInto(GithubRepository::class),
fn(): ApiError => ApiError::fromResponse($response)
);
}
}
Wrapping Up
Consistency and abstraction can turn your codebase from good to great! By using a structured pattern like this, you're not just saving time today, you're saving your future self or your teammates.
If you're tired of messy integrations, give the motomedialab/connector a try. There's more comprehensive documentation there and I'd love to hear how you're using it to streamline your integrations!
If you enjoyed this article, please consider subscribing for more.