This article was originally written by Devin Gray on the Honeybadger Developer Blog.
All developers will eventually need to integrate an app with a third-party API. However, the APIs that we are trying to make sense of often do not provide a lot of flexibility. This article aims to address this issue within your codebase to seamlessly integrate apps into any API using Laravel.
What we aim to achieve
Previously, this blog published a great article on Consuming APIs in Laravel with Guzzle, which showcases how Laravel can handle API calls out of the box. If you are looking for a way to do simple API calls and have not yet given this article a read, I would suggest starting there. In this article, we will build on top of this idea and learn how to structure our codebase to make use of a more SOLID approach to handling third-party APIs.
Structuring the application
Before we start creating files, we will install a third-party package that will help a ton. Laravel Saloon is a package created by Sam Carré that works as a middleman between Laravel and any third-party API to allow us to build an incredible developer experience around these APIs.
Installation instructions to get this package into your project can be found here.
Once this package is installed, we can start building. The idea behind Laravel Saloon is that each third-party API will consist of a connector and multiple requests. The connector class will act as the base class for all requests, while each request will act as a specified class for each endpoint. These classes will live in the app/Http/Integrations
folder of our app. With this said, we are ready to look at an example for our new app.
Get connected
For our project, we will use Rest Countries, which is a simple and free open source API. To get started, we need to create the Laravel Saloon Connector:
php artisan saloon:connector Countries CountriesConnector
This will create a new file in app/Http/Integrations/Countries
, which will now contain a single class called CountriesConnector
. This file will be the base for all requests to this API. It acts as a single place to define any configurations or authentications required to connect to this API. By default, the file looks like this:
<?php
namespace App\Http\Integrations\Countries;
use Sammyjo20\Saloon\Http\SaloonConnector;
use Sammyjo20\Saloon\Traits\Plugins\AcceptsJson;
class CountriesConnector extends SaloonConnector
{
use AcceptsJson;
public function defineBaseUrl(): string
{
return '';
}
public function defaultHeaders(): array
{
return [];
}
public function defaultConfig(): array
{
return [];
}
}
We want to change the defineBaseUrl
method to now return the base URL for our API:
public function defineBaseUrl(): string
{
return 'https://restcountries.com/v3.1/';
}
For this example, we do not need to do anything more, but in the real world, this would most likely be the file where you can add authentication for the external API.
Making requests
Now that we have our connector set up, we can make our first request class. Each request will act as a different endpoint for the API. In this case, we will make use of the 'All' endpoint on the Rest Countries API. The full URL is as follows:
https://restcountries.com/v3.1/all
To get started building a request class, we can once again use the provided artisan commands:
php artisan saloon:request Countries ListAllCountriesRequest
This will generate a new file in app/Http/Integrations/Countries/Requests
called ListAllCountriesRequest
. By default, the file looks like this:
<?php
namespace App\Http\Integrations\Countries\Requests;
use Sammyjo20\Saloon\Constants\Saloon;
use Sammyjo20\Saloon\Http\SaloonRequest;
class ListAllCountriesRequest extends SaloonRequest
{
protected ?string $connector = null;
protected ?string $method = Saloon::GET;
public function defineEndpoint(): string
{
return '/api/v1/user';
}
}
This file works as an independent class for each endpoint provided by the API. In our case, the endpoint will be all
, so we need to update the defineEndpoint
method:
public function defineEndpoint(): string
{
return 'all';
}
For this request class to know which connection to make the request from, we need to update the $connector
to reflect the connector we built in the previous step:
protected ?string $connector = CountriesConnector::class;
Don't forget to import your class by adding
use App\Http\Integrations\Countries\CountriesConnector;
at the top of the file
Now that we have our first connector and our first request ready, we can make our very first API call.
Making API calls using the connector and request
To do an initial test to see if the API is working as expected, let's alter our routes/web.php
file to simply return the response to us when we load up the application.
<?php
use App\Http\Integrations\Countries\Requests\ListAllCountriesRequest;
use Illuminate\Support\Facades\Route;
Route::get('/', function () {
$request = new ListAllCountriesRequest();
return $request->send()->json();
});
We can now see a JSON dump from the API! It’s as easy as that!
What we have learned so far
So far, we can see that our application can make an API call to https://restcountries.com/v3.1/all
and display the results given using two PHP classes. What is really great about this is that the structure of our app remains "The Laravel Way" and keeps each type of API call separate, which allows us to separate concerns in our application. Our codebase is simple, with the following structure:
app
-- Http
--- Integrations
---- Requests
----- ListAllCountriesRequest.php
---- CountriesConnector.php
Adding more requests to the same API is a matter of creating a new Request class, which makes the whole developer experience a breeze. However, we could still take this further.
Making use of data transfer objects
When making API requests, a common problem developers encounter is that you will be given data that are not formatted or easily usable within an application. This unstructured data makes it fairly difficult to work with in our application. To combat this problem, we can make use of something called a data transfer object (DTO). Doing this will allow us to map the response of our API call to a PHP object that can stand alone. It will prevent us from having to write code that looks like this:
$name = $response->json()[0]['name'];
Instead, it will allow us to write code that looks like this, which is a lot cleaner and easier to work with:
$name = $response->name;
Let's dive in and make our response as clean as a whistle. To do this, we will create a new API call on the CountriesConnector
to find a country given a name.
php artisan saloon:request Countries GetCountryByNameRequest
Following the steps outlined above, my class will now look like this:
<?php
namespace App\Http\Integrations\Countries\Requests;
use App\Http\Integrations\Countries\CountriesConnector;
use Sammyjo20\Saloon\Constants\Saloon;
use Sammyjo20\Saloon\Http\SaloonRequest;
class GetCountryByNameRequest extends SaloonRequest
{
public function __construct(public string $name)
{
}
protected ?string $connector = CountriesConnector::class;
protected ?string $method = Saloon::GET;
public function defineEndpoint(): string
{
return 'name/' . $this->name;
}
}
Making use of this request is easy, and it can be done simply like this:
$request = new GetCountryByNameRequest('peru');
return $request->send()->json();
The full body of this response is quite a large JSON object, and the full object can be seen below:
Therefore, in our next step, let's take the data above and map it to a DTO to keep things clean.
Casting to DTOs
The first thing we need is a DTO class. To make one of these, simply create a new Class in your preferred location. For me, I like to keep them in app/Data
, but you are free to put them wherever works best for your project.
<?php
namespace App\Data;
class Country
{
public function __construct(
public string $name,
public string $officalName,
public string $mapsLink,
){}
}
For this example, we will map these three items to our DTO.
- Name
- Official Name
- Maps Link for Google maps
All of these items are present within our JSON Response. Now that we have our base DTO available and ready to use, we can begin by using a trait and a method on our request. Our final request class will look like this:
<?php
namespace App\Http\Integrations\Countries\Requests;
use App\Data\Country;
use App\Http\Integrations\Countries\CountriesConnector;
use Sammyjo20\Saloon\Constants\Saloon;
use Sammyjo20\Saloon\Http\SaloonRequest;
use Sammyjo20\Saloon\Http\SaloonResponse;
use Sammyjo20\Saloon\Traits\Plugins\CastsToDto;
class GetCountryByNameRequest extends SaloonRequest
{
use CastsToDto;
public function __construct(public string $name)
{
}
protected ?string $connector = CountriesConnector::class;
protected ?string $method = Saloon::GET;
public function defineEndpoint(): string
{
return 'name/' . $this->name;
}
protected function castToDto(SaloonResponse $response): object
{
return Country::fromSaloon($response);
}
}
From here, we need to make the mapping on our DTO class, so we can add the fromSaloon
method onto the DTO itself like this:
<?php
namespace App\Data;
use Sammyjo20\Saloon\Http\SaloonResponse;
class Country
{
public function __construct(
public string $name,
public string $officalName,
public string $mapsLink,
)
{}
public static function fromSaloon(SaloonResponse $response): self
{
$data = $response->json();
return new static(
name: $data[0]['name']['common'],
officalName: $data[0]['name']['official'],
mapsLink: $data[0]['maps']['googleMaps']
);
}
}
Now, when we want to make use of our API, we will know what the data will look like when it is returned. In our case:
$request = new GetCountryByNameRequest('peru');
$response = $request->send();
$country = $response->dto();
return new JsonResponse($country);
Will return the following JSON object:
{
"name": "Peru",
"officalName": "Republic of Peru",
"mapsLink": "https://goo.gl/maps/uDWEUaXNcZTng1fP6"
}
This is a lot cleaner and easier to work with than the original multi-nested object. This method has a ton of use cases that can be applied directly onto it. The DTO classes can house multiple methods that allow you to interact with the data all in one place. Simply add more methods to this class, and you have all of your logic in one place. For more information on this topic, Laravel Saloon has written a full integration guide on DTOs, which can be found here.
API testing using mocked classes
The last point that I would like to cover is probably one of the most painful points for developers. That is, "How to test an API?". When testing APIs, it is normally a good practice to ensure that you do not make an actual HTTP request in the test suite, as this can cause all kinds of errors. Instead, what you want to do is use a 'mock' class to pretend that the API was sent.
Let's take a look based on our above example. Using Laravel Saloons built-in testing helpers, we can do a full range of tests by adding the following in our test cases:
use Sammyjo20\SaloonLaravel\Facades\Saloon;
use Sammyjo20\Saloon\Http\MockResponse;
$fakeData = [
[
'name' => [
'common' => 'peru',
'official' => 'Republic of Peru'
],
'maps' => [
'googleMaps' => 'https://example.com'
]
]
];
Saloon::fake([
GetCountryByNameRequest::class => MockResponse::make($fakeData, 200)
]);
(new GetCountryByNameRequest('peru'))->send()
With the above code, we have made a "Mock" of our Request. This means that any time we call the request, regardless of what data are provided, Saloon will always return the response with the fake data. This helps us to know that our requests are working as expected without having to make real API calls to live environments.
With this approach, you can test both failed responses and responses where the data may not be available. This will ensure you have covered all areas of your codebase. For more information on how to do testing, check out the testing documentation for Laravel. For a more in-depth guide on testing with Laravel Saloon, have a look at their extensive documentation.
Conclusion
Given the scope of how difficult it is to integrate multiple APIs, I truly hope that this article will provide a deeper understanding of how this can be done in the most simple form. Laravel has so many amazing packages that make the lives of their developers easier, and Laravel Saloon is one of them. Integrating APIs in a clean and scalable way has never been easier.
Top comments (0)