Symfony applications often need to call another HTTP API from a controller, command, message handler, or scheduled job. The HttpClient component keeps that request code inside an injectable service, where the target URL, headers, timeout, status handling, and JSON parsing can be reused instead of scattered through application entry points.
The framework exposes the http_client service when symfony/http-client is installed, and autowiring injects it through HttpClientInterface. A request() call starts the transfer immediately, while reading the status, headers, content, or decoded array is the point where the calling code waits for the response.
A controlled JSON fixture makes the first smoke test deterministic before a real API token or vendor endpoint enters the project. The client service reads CATALOG_API_BASE_URI from the environment, sends an Accept: application/json request, rejects unexpected status codes, and returns decoded product rows to a console command.
$ composer require symfony/http-client
$ mkdir -p public/demo
{
"products": [
{
"id": 101,
"name": "Desk lamp"
},
{
"id": 102,
"name": "Cable organizer"
}
]
}
Save the file as public/demo/products.json.
CATALOG_API_BASE_URI=http://127.0.0.1:8000/demo
Use the real HTTPS API base URI in each deployed environment. Keep secrets such as bearer tokens in separate environment variables instead of hard-coding them in PHP source.
Related: How to set Symfony environment variables
$ mkdir -p src/Service
<?php namespace App\Service; use Symfony\Component\DependencyInjection\Attribute\Autowire; use Symfony\Contracts\HttpClient\HttpClientInterface; final class CatalogApiClient { public function __construct( private readonly HttpClientInterface $client, #[Autowire('%env(CATALOG_API_BASE_URI)%')] private readonly string $baseUri, ) { } /** * @return list<array{id: int, name: string}> */ public function fetchProducts(): array { $response = $this->client->request('GET', rtrim($this->baseUri, '/').'/products.json', [ 'headers' => [ 'Accept' => 'application/json', ], 'max_duration' => 5, ]); $statusCode = $response->getStatusCode(); if (200 !== $statusCode) { throw new \RuntimeException(sprintf('Catalog API returned HTTP %d.', $statusCode)); } $data = $response->toArray(); return $data['products'] ?? []; } }
Save the file as src/Service/CatalogApiClient.php. Per-request options in the third request() argument override wider client defaults for this call.
$ mkdir -p src/Command
<?php namespace App\Command; use App\Service\CatalogApiClient; use Symfony\Component\Console\Attribute\AsCommand; use Symfony\Component\Console\Command\Command; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Style\SymfonyStyle; use Symfony\Contracts\HttpClient\Exception\DecodingExceptionInterface; use Symfony\Contracts\HttpClient\Exception\HttpExceptionInterface; use Symfony\Contracts\HttpClient\Exception\TransportExceptionInterface; #[AsCommand( name: 'app:catalog:fetch', description: 'Fetch product data from the catalog API.', )] final class FetchCatalogCommand extends Command { public function __construct( private readonly CatalogApiClient $catalogApiClient, ) { parent::__construct(); } protected function execute(InputInterface $input, OutputInterface $output): int { $io = new SymfonyStyle($input, $output); try { $products = $this->catalogApiClient->fetchProducts(); } catch (DecodingExceptionInterface|HttpExceptionInterface|TransportExceptionInterface|\RuntimeException $exception) { $io->error($exception->getMessage()); return Command::FAILURE; } foreach ($products as $product) { $io->writeln(sprintf('%d %s', $product['id'], $product['name'])); } return Command::SUCCESS; } }
Save the file as src/Command/FetchCatalogCommand.php. TransportExceptionInterface covers network failures, HttpExceptionInterface covers HTTP response exceptions, and DecodingExceptionInterface covers invalid JSON.
$ php bin/console cache:clear // Clearing the cache for the dev environment with debug true [OK] Cache for the "dev" environment (debug=true) was successfully cleared.
Related: How to clear Symfony cache
$ php bin/console list app Symfony v8.1.0 (env: dev, debug: true) ##### snipped ##### Available commands for the "app" namespace: app:catalog:fetch Fetch product data from the catalog API.
$ symfony server:start --no-tls --port=8000 -d
[OK] Web server listening
http://127.0.0.1:8000
When 8000 already belongs to another local project, choose another free port and set CATALOG_API_BASE_URI to the matching URL.
Related: How to run a Symfony project locally
$ curl -i -sS http://127.0.0.1:8000/demo/products.json
HTTP/1.1 200 OK
Content-Type: application/json
##### snipped #####
{
"products": [
{
"id": 101,
"name": "Desk lamp"
},
{
"id": 102,
"name": "Cable organizer"
}
]
}
$ php bin/console app:catalog:fetch 101 Desk lamp 102 Cable organizer
The two product rows confirm that the command received the response, decoded the JSON body, and read the products array through CatalogApiClient.
$ symfony server:stop [OK] Stopped 2 process(es) successfully
$ rm -r public/demo
Run this only for the temporary public/demo directory created for the smoke test, after CATALOG_API_BASE_URI points at the real API base URI.