Building A SOLID foundation for API's in Laravel
Tags: php, laravel
Published: 2024-02-13 08:00:00

*This article was originally written by [Devin Gray](https://www.honeybadger.io/blog/authors/devingray/) on the [Honeybadger Developer Blog](https://www.honeybadger.io/blog/laravel-apis/).*

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](https://www.honeybadger.io/blog/laravel-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](https://www.digitalocean.com/community/conceptual-articles/s-o-l-i-d-the-first-five-principles-of-object-oriented-design) 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](https://github.com/sammyjo20/saloon) is a package created by [Sam Carré](https://twitter.com/carre_sam) 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](https://docs.saloon.dev/getting-started/getting-started).

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](https://restcountries.com), which is a simple and free open source API. To get started, we need to create the Laravel Saloon Connector:

```bash
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
<?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:

```php
    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:

```bash
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
<?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:

```php
    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:

```php
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
<?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!

![JSON Dump](https://www.honeybadger.io/images/blog/posts/laravel-apis/json_dump.png)

## 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:

```bash
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:

```php
$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:

```php
$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.

```bash
php artisan saloon:request Countries GetCountryByNameRequest
```

Following the steps outlined above, my class will now look like this:

```php
<?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:

```php
$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:

![JSON Dump](https://www.honeybadger.io/images/blog/posts/laravel-apis/json_dump_single.png)

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
<?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
<?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
<?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:

```php
$request = new GetCountryByNameRequest('peru');
$response = $request->send();
$country = $response->dto();
return new JsonResponse($country);
```

Will return the following JSON object:

```json
{
  "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](https://docs.saloon.dev/the-basics/responses/data-transfer-objects).

## 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:

```php
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](https://laravel.com/docs/9.x/testing) for Laravel.  For a more in-depth guide on testing with Laravel Saloon, have a look at their [extensive documentation](https://docs.saloon.dev/next-steps/testing-faking-responses/laravel).

## 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.