DEV Community

Honeybadger Staff for Honeybadger

Posted on • Originally published at honeybadger.io

Building A SOLID foundation for API's in Laravel

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
Enter fullscreen mode Exit fullscreen mode

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 [];
    }
}
Enter fullscreen mode Exit fullscreen mode

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/';
    }
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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';
    }
}
Enter fullscreen mode Exit fullscreen mode

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';
    }
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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();
});
Enter fullscreen mode Exit fullscreen mode

We can now see a JSON dump from the API! It’s as easy as that!

JSON Dump

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
Enter fullscreen mode Exit fullscreen mode

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'];
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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
Enter fullscreen mode Exit fullscreen mode

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;
    }
}
Enter fullscreen mode Exit fullscreen mode

Making use of this request is easy, and it can be done simply like this:

$request = new GetCountryByNameRequest('peru');
return $request->send()->json();
Enter fullscreen mode Exit fullscreen mode

The full body of this response is quite a large JSON object, and the full object can be seen below:

JSON Dump

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,

    ){}
}
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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']
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

Will return the following JSON object:

{
  "name": "Peru",
  "officalName": "Republic of Peru",
  "mapsLink": "https://goo.gl/maps/uDWEUaXNcZTng1fP6"
}
Enter fullscreen mode Exit fullscreen mode

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()
Enter fullscreen mode Exit fullscreen mode

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)