How to create an HTTP client request in Symfony

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.

Steps to create a Symfony HTTP client request:

  1. Open a terminal in the Symfony project root that contains bin/console.
  2. Require the HttpClient component when the project does not already include it.
    $ composer require symfony/http-client
  3. Create a temporary public endpoint directory for the smoke test.
    $ mkdir -p public/demo
  4. Add a JSON response fixture.
    products.json
    {
      "products": [
        {
          "id": 101,
          "name": "Desk lamp"
        },
        {
          "id": 102,
          "name": "Cable organizer"
        }
      ]
    }

    Save the file as public/demo/products.json.

  5. Set the local API base URI.
    .env.local
    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

  6. Create the service directory when it does not already exist.
    $ mkdir -p src/Service
  7. Add the HTTP client service.
    CatalogApiClient.php
    <?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.

  8. Create the command directory when it does not already exist.
    $ mkdir -p src/Command
  9. Add a console command that uses the client service.
    FetchCatalogCommand.php
    <?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.

  10. Rebuild the development cache.
    $ 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.
  11. Confirm that Symfony registered the command.
    $ 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.
  12. Start the local Symfony web server.
    $ 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

  13. Check that the fixture endpoint returns JSON.
    $ 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"
        }
      ]
    }
  14. Run the command that sends the HTTP client request.
    $ 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.

  15. Stop the local server after the smoke test.
    $ symfony server:stop
    [OK] Stopped 2 process(es) successfully
  16. Remove the temporary endpoint fixture.
    $ 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.