Home » Laravel » How to Request to External API in Laravel

How to Request to External API in Laravel

You must often interact with different third-party APIs when developing various services. You can use file_get_contents or the curl extension for PHP to do this. Also, for many APIs, developers release libraries that you can use. But the use of low-level functions makes the code complex. On the other hand, you might have no access to all the settings when using libraries, especially non-official ones.

The Laravel framework already includes the guzzlehttp/guzzle package and an HTTP facade that can help to make API requests more easily. In this article, I’ll show you how to use this facade to request to external API in Laravel application and how to test code that uses it.

Table of Contents

How to Perform Requests Using Http

In this article, I will use The Solar System OpenData API for examples. It is an open API that does not require registration and authentication. Additionally, it is straightforward. You can find the Swagger documentation for it here.

1. Simple Request

If you need to do a simple get request to API, just call the static get() method of the Laravel HTTP client.

app/Console/Commands/TestRequestCommand.php$response = \Http::get("https://api.le-systeme-solaire.net/rest/bodies");

2. Prepare Request

Unlike Guzzle Http client, where a client object can be used for several requests, the laravel’s wrapper for Guzzle constructs each request separately. But if you need to perform a few requests, you can make a separate method that applies basic settings. For example, you can set baseUrl() and then provide only the last part of the path:

app/Console/Commands/TestRequestCommand.php$request = \Http::baseUrl("https://api.le-systeme-solaire.net/rest/"); $response = $request->get("bodies");

Notice that the path shouldn’t start from a slash. When you add a slash (“/bodies”), only the domain will be taken from the base URL, and you will get an incorrect external URL. For instance, it will be https://api.le-systeme-solaire.net/bodies when you expect it to be https://api.le-systeme-solaire.net/rest/bodies.

3. Handle Response

You can use the successful() method in the response object to ensure that the request was successful, and then call body() to get a remote server response as a string.

app/Console/Commands/TestRequestCommand.php$response = \Http::get("https://api.le-systeme-solaire.net/rest/bodies"); if ($response->successful()) { dump("success"); $data = $response->body(); }

Additionally, you can convert JSON response to an associative array or fetch a specific field from JSON data:

app/Console/Commands/TestRequestCommand.php$response->json(); $response->json("message");

4. Query Parameters

When you need to send query parameters to the server in your api request, you can pass them as an array in the second argument for the get() method:

app/Console/Commands/TestRequestCommand.php$request->get("bodies", ["data" => "id"]);

Here is the full Laravel call get request API example that fetches identifiers of all bodies in The Solar System:

app/Console/Commands/TestRequestCommand.php$response = \Http::baseUrl("https://api.le-systeme-solaire.net/rest/")->get( "bodies", ["data" => "id"] ); if ($response->successful()) { dump("success"); dump($response->json()); }

5. Sending JSON

To send JSON in a request, you need to set raw request body using the withBody() method. If your JSON structure is simple, you can serialize it from an array using json_encode(). For example:

app/Console/Commands/TestRequestCommand.php$data = ["data" => "id"]; $response = \Http::withBody(json_encode($data), "application/json")->post("/some/query");

However, multidimensional arrays may be inconvenient if you have a complex structure of nested objects. In that case, it would be better to use objects and serialize them by jms/serializer package.

I won’t go into detail here about how to use Laravel’s HTTP client. The basic usage is simple. You can find all the additional information in the official documentation. Instead, let’s look at the project’s structure and how to use the HTTP Client capabilities to access the data from third-party APIs easily. This facade takes care of almost all the work of processing network requests and errors, and we only have to convert the result to the desired value.

How to Request to External API in Laravel

1. Project structure

I usually create a separate client class containing all API interaction methods in my projects. Each API endpoint has a separate method that returns an object containing not only data obtained from API but also metadata about the request. The data from API is returned not as a string or array but as an object too.

It makes a project more structured, easier to maintain, and testable. Most IDE can auto-complete object fields, so when using this client, you won’t need to look at its code to understand how to get desired data.

The advantage of returning some metadata from a request is that you don’t have to think about how to catch different exceptions that may occur and extract data from them. Also, when writing tests, you can mock your class and return the object with desired data instead of making requests to the API.

Classes that contain only fields and are used for data transferring are called DTOs (Data Transfer Objects). PHP 8.0 brings the ability to declare read-only properties in the constructor, which makes the creation of DTOs simpler.

So, to get a list of celestial bodies in the solar system and information about them from the Solar System API, we need the following classes:

  • SolarSystemClient – the client class;
  • IdsList – data transfer object with list of identifiers;
  • BodyInfo – data transfer object with information about a celestial body.

2. Creating DataTransfers

Laravel built-in HTTP client simplifies error handling by catching the RequestExpeption from Guzzle, which is thrown whenever the server returns an error status code (not 2xx). Nevertheless, ConnectException and JSON parsing errors are not handled here.

Usually, I prefer to handle all errors in the API client class and return unified error status and message in DTO. Therefore, in this example, I will add the $success and $message fields to data transfer objects. The first will contain false if the request fails, and the second will contain an error message. Here is what the IdsList class will look like:

app/DataTransfers/IdsList.php<?php namespace App\DataTransfers; class IdsList { public function __construct( public readonly bool $success, public readonly array $ids = [], public readonly ?string $message = null, ){} }

In the code above the $ids field contain an array of celestial body identifiers. Everything else is metadata. You can add as much metadata here as you need, for example, the exception class, status code, or even the entire server response with headers as a string. This can be used for debugging in the future. You can look at the data and understand where the error is. Now, let’s look at the BodyInfo class:

app/DataTransfers/BodyInfo.php<?php namespace App\DataTransfers; class BodyInfo { public function __construct( public readonly bool $success, public readonly ?string $id = null, public readonly ?string $name = null, public readonly ?bool $isPlanet = null, public readonly ?int $density = null, public readonly ?int $gravity = null, public readonly ?string $bodyType = null, public readonly ?string $message = null ) { } }

Here, you can see more data fields and the same metadata.

3. Creating Client Class

As I said before, the Http facade does not have a client which can be configured once and used for all requests. Using the Illuminate\Http\Client\PendingRequest class, you must build each request separately. Usually, it is needed to add a base API URL, authorization headers, middleware, or other parameters, which are actual for all requests. To solve this issue, you can create a separate method that will initialize the request with desired settings:

app/Services/Clients/SolarSystemClient.phpuse Illuminate\Http\Client\PendingRequest; use Illuminate\Support\Facades\Http; use App\DataTransfers\BodyInfo; use App\DataTransfers\IdsList;

app/Services/Clients/SolarSystemClient.phpprivate function makeRequest(): PendingRequest { return Http::baseUrl("https://api.le-systeme-solaire.net/rest/"); }

Now, let’s take a look at the getBodyIds() method, which uses the method we just created:

app/Services/Clients/SolarSystemClient.phppublic function getBodyIds(): IdsList { try { $request = $this->makeRequest(); $response = $request->get("bodies", ["data" => "id"]); if ($response->successful()) { return new IdsList( success: true, ids: $response ->collect("bodies") ->pluck("id") ->toArray() ); } else { return new IdsList( success: false, ids: [], message: $response->json("message", $response->body()) ); } } catch (\Throwable $e) { return new IdsList( success: false, ids: [], message: $e->getMessage() ); } }

Here, all possible errors are handled, except if the server returns an invalid JSON object with code 200. But this shouldn’t happen in a well-designed API. First, the request is executed. Then, if it was successful, we used the built-in methods of working with collections to transform JSON into a list of identifiers. If the request is unsuccessful, we try to get the value of the message field when it exists in returned JSON. If the JSON cannot be parsed, we put the entire content of the server response as a string in the message field.

And this is how the method for obtaining information about a particular object in the solar system will look like:

app/Services/Clients/SolarSystemClient.phppublic function getBodyInfo(string $id): BodyInfo { $response = $this->makeRequest()->get("bodies/$id"); try { if ($response->successful()) { return new BodyInfo( success: true, id: $response->json("id"), name: $response->json("name"), isPlanet: $response->json("isPlanet"), density: $response->json("density"), gravity: $response->json("gravity"), bodyType: $response->json("bodyType") ); } else { return new BodyInfo( success: false, message: $response->json("message", $response->body()) ); } } catch (\Throwable $e) { return new BodyInfo(success: false, message: $e->getMessage()); } }

This method works as well as the previous one. Now you need to ensure that this class works and returns the requested data. To do this, you can create a command and call the client methods there, but writing a test is better because you can re-run it later. To create the test, run the following command:

php artisan make:test SolarSystemWithRequestTest

The command will create the SolarSystemGetTest file in the tests/Feature directory. Add the following method to it:

tests/Feature/SolarSystemWithRequestTest.phppublic function testGetBodiesWorksCorrectly() { $client = new \App\Services\Clients\SolarSystemClient(); $data = $client->getBodyIds(); $this->assertTrue($data->success); dump($data); }

Then add another method which will ensure that fetching information about a specific celestial body will work too:

tests/Feature/SolarSystemWithRequestTest.phppublic function testGetBodyInfoWorksCorrectly() { $client = new \App\Services\Clients\SolarSystemClient(); $info = $client->getBodyInfo("titan"); $this->assertTrue($info->success); dump($info); }

Then, run both test cases:

php artisan test --filter SolarSystemWithRequestTest

4. Logging Requests

Sometimes, you need to record in the database or see in real time what was sent in the request and what the server returned. To outgoing http requests and their responses in a specific client, you can use a Middleware closure or the GuzzleHttp\Middleware class with the withMiddleware() method. For example, let’s add middleware that will record all requests and responses into a Laravel log file in the makeRequest() method:

app/Services/Clients/SolarSystemClient.phpuse GuzzleHttp\Middleware; use Psr\Http\Message\RequestInterface; use Psr\Http\Message\ResponseInterface; use Illuminate\Support\Facades\Log;

app/Services/Clients/SolarSystemClient.php$request = Http::baseUrl("https://api.le-systeme-solaire.net/rest/"); $request->withMiddleware( Middleware::mapRequest(function (RequestInterface $request) { Log::info("Request:", [ "method" => $request->getMethod(), "url" => (string) $request->getUri(), "headers" => $request->getHeaders(), "body" => (string) $request->getBody(), ]); return $request; }) ); $request->withMiddleware( Middleware::mapResponse(function (ResponseInterface $response) { Log::info("Response:", [ "statusCode" => $response->getStatusCode(), "headers" => $response->getHeaders(), "body" => (string) $response->getBody(), ]); return $response; }) ); return $request;

By default, the Log facade writes messages into the storage/laravel.log file. You can re-run tests and ensure that requests and responses are written:

php artisan test --filter SolarSystemWithRequestTest

Here you can do everything you need with the RequestInterface and ResponseInterface objects. The mapRequest closure is called before the request is sent, so you can modify it and add a signature or some other authorization fields. It is the same with the mapResponse closure. It will be called before the result is delivered to your client code.

Pay attention that after reading from streaming interfaces in Middleware, you should call the rewind() method for a stream to reset it to the state to initial. In this case, there will be no problems even if you do not call these methods, but it is still worth doing.

5. Testing Client Class

I’ve already shown examples of tests above, but not all APIs can be called an infinite number of times. There are a lot of paid APIs that charge money per request, so you often need a way to test your code without really calling an API.

Http facade that the fake() method allows returning fake responses for specific request uri or even for all requests. Let’s create a GetBodyIdsDummyTest and add the testGetBodyIdsWorksCorrectly() method that will check that the client process correct JSON with a list of object IDs in the solar system properly:

php artisan make:test GetBodyIdsDummyTest

tests/Feature/GetBodyIdsDummyTest.phppublic function testGetBodyIdsWorksCorrectly() { $data = <<<STR { "bodies": [ { "id": "lune" }, { "id": "phobos" }, { "id": "deimos" }, { "id": "io" }, { "id": "europe" }, { "id": "ganymede" }, { "id": "callisto" } ] } STR; \Http::fake([ "https://api.le-systeme-solaire.net/rest/*" => \Http::response( $data, 200 ), ]); $client = new \App\Services\Clients\SolarSystemClient(); $response = $client->getBodyIds(); $this->assertCount(7, $response->ids); $this->assertTrue($response->success); $this->assertEquals("lune", $response->ids[0]); \Http::assertSent(function (\Illuminate\Http\Client\Request $request) { return $request->url() == "https://api.le-systeme-solaire.net/rest/bodies?data=id"; }); }

If there is only one request, so it is unnecessary to specify the full URL. Just specify an asterisk *. After the request, we ensured the request was successful, and the client collected seven objects. In addition, using the assertSent() method, you can make sure that the data you need is sent.

Now, let’s have a look at how our client will handle 500 server errors:

tests/Feature/GetBodyIdsDummyTest.phppublic function testGetBodyIdsHandlesNonSuccessStatusCode() { \Http::fake([ "*" => \Http::response('{"message": "Unknown error"}', 500), ]); $client = new \App\Services\Clients\SolarSystemClient(); $response = $client->getBodyIds(); $this->assertFalse($response->success); $this->assertEquals("Unknown error", $response->message); }

You can also test that the ConnectException is also handled correctly:

tests/Feature/GetBodyIdsDummyTest.phppublic function testGetBodyIdsHandlesConnectError() { \Http::fake([ "*" => fn($request) => new \GuzzleHttp\Promise\RejectedPromise( new \GuzzleHttp\Exception\ConnectException( "Failed to connect to the server!", $request->toPsrRequest() ) ), ]); $client = new \App\Services\Clients\SolarSystemClient(); $result = $client->getBodyIds(); $this->assertFalse($result->success); $this->assertEquals("Failed to connect to the server!", $result->message); }

Similarly, you can check any request your client sends and ensure that everything is fine using fake API responses without requesting an external API. You may not be able to find all the errors this way, but you can definitely minimize them. Run the following artisan command to test cases from this class:

php artisan test --filter GetBodyIdsDummyTest

6. Mocking Client Class

When you’re sure that the client class works fine, and you need to test any other class which uses your client, you can replace the client with Mockery and provide a DTO with the desired data. For example, let’s create a SolarSystemService class that handles possible errors and then returns a list of celestial body identifiers:

app/Services/SolarSystemService.php<?php namespace App\Services; use App\Services\Clients\SolarSystemClient; class SolarSystemService { private SolarSystemClient $client; public function __construct(SolarSystemClient $client) { $this->client = $client; } public function getBodyIds(): array { $response = $this->client->GetBodyIds(); if (false === $response->success) { throw new RuntimeException($response->message); } //Do something else or: return $response->ids; } }

php artisan make:test SolarSystemServiceTest

tests/Feature/SolarSystemServiceTest.phppublic function testListingSolarSystemBodiesWorksCorrectly() { $client = $this->mock( \App\Services\Clients\SolarSystemClient::class, function ($mock) { $mock->shouldReceive("getBodyIds")->andReturnUsing(function () { $bodies = new \App\DataTransfers\IdsList( success: true, ids: ["foo", "bar", "baz"] ); return $bodies; }); } ); $service = new \App\Services\SolarSystemService($client); $ids = $service->getBodyIds(); dump($ids); $this->assertCount(3, $ids); $this->assertEquals("foo", $ids[0]); }

php artisan test --filter SolarSystemServiceTest

Wrapping Up

In this article, we’ve looked at how to request external API in Laravel using its built-in Http client. This facade greatly simplifies the preparation and execution of requests, and now you know how to use it conveniently and efficiently. What libraries and approaches do you use to do it? Let me know in the comments!

Leave a Comment