How to Connect Apigee from Your Drupal Website

Drupal

How to Connect Apigee from Your Drupal Website

A practical Drupal guide for calling APIs through Google Cloud Apigee, including proxy setup, API key and OAuth options, Drupal service code, deployment checks, and common troubleshooting challenges.

Drupal should not usually call fragile backend endpoints directly from random controllers, templates, or JavaScript snippets. A cleaner pattern is to put Apigee in front of the backend, let Apigee enforce the API contract, and let Drupal call the Apigee proxy through a small reusable service.

Architecture diagram showing a Drupal website connecting through Apigee to backend services.
Recommended flow: Drupal calls an Apigee proxy, Apigee applies security and traffic policies, and the proxy forwards the request to backend services.

What We Are Building

The goal is a server-side Drupal integration that calls an API through Apigee. Apigee acts as the managed API gateway between Drupal and your backend services. In Google Cloud's Apigee documentation, Apigee is described as an API management platform used to build, manage, and secure APIs, with API proxies providing a consistent interface for backend services.

The final architecture looks like this:

Drupal page/form/block/controller
  -> custom Drupal service
  -> Drupal HTTP client
  -> Apigee proxy URL
  -> Apigee policies: API key, OAuth, quota, spike arrest, logging
  -> backend service
  -> response normalized by Apigee
  -> Drupal render array/cache/data model

When This Pattern Makes Sense

  • You have a Drupal website that needs data from an internal, partner, or cloud API.
  • You want Apigee to control authentication, quotas, analytics, traffic shaping, and fault handling.
  • You do not want API credentials exposed in browser JavaScript.
  • You want backend services to evolve without forcing Drupal to know every backend detail.

Prerequisites

  • A Drupal 10 or Drupal 11 site where you can add a custom module.
  • An Apigee organization and environment.
  • An Apigee API proxy deployed to an environment group hostname.
  • An API product that includes the proxy or resource paths Drupal is allowed to call.
  • A developer app in Apigee with credentials for Drupal.
  • A decision about authentication: API key for simpler app identification, or OAuth 2.0 client credentials for stronger server-to-server authentication.

Step 1: Create the Apigee Proxy

In Apigee, create an API proxy that points to the backend service Drupal needs. Keep the public proxy path stable and simple, for example:

https://api.example.com/content/v1/articles
https://api.example.com/customer/v1/profile
https://api.example.com/search/v1/results

Inside the proxy, configure the target endpoint to your backend. The backend can be a service running in Google Cloud, an internal service exposed through networking, a legacy API, or a third-party endpoint. The point is that Drupal calls Apigee, not the backend directly.

Step 2: Protect the Proxy

For a quick internal integration, API key verification is common. Apigee calls API keys consumer keys. The client app passes the key with each request, and Apigee validates it against the developer app and API product. Google notes that API keys are useful as app identifiers, but they are limited as security tokens because they can be extracted from client-side code. For Drupal, that means API keys should be stored server-side and never rendered into browser JavaScript.

A simple VerifyAPIKey policy can read the key from a header:

<VerifyAPIKey name="Verify-Drupal-Key">
  <APIKey ref="request.header.x-apikey" />
</VerifyAPIKey>

Attach that policy to the proxy request flow before the target is called. Prefer a header such as x-apikey over a query parameter, because query strings are more likely to appear in logs, browser history, analytics, and shared URLs.

For production server-to-server integrations, OAuth 2.0 client credentials is usually a better choice. Apigee's OAuthV2 policy supports operations such as generating access tokens and verifying access tokens. With this pattern, Drupal exchanges a client ID and client secret for a short-lived bearer token, caches that token, and sends it to the protected API proxy.

Step 3: Create the API Product and Developer App

In Apigee:

  1. Create an API product for the Drupal integration.
  2. Add the API proxy and the resource paths Drupal is allowed to call.
  3. Set quota limits that match Drupal's expected traffic.
  4. Create a developer app named something like drupal-website-prod.
  5. Attach the API product to that developer app.
  6. Copy the consumer key, and if using OAuth, the consumer secret.

Use separate developer apps for local, staging, and production. That makes revocation and traffic analysis much easier.

Step 4: Store Drupal Configuration Safely

Do not hard-code Apigee credentials in a controller, template, JavaScript file, or committed config export. Use environment-specific settings.

In settings.php or settings.local.php:

$settings['apigee'] = [
  'base_uri' => getenv('APIGEE_BASE_URI') ?: 'https://api.example.com',
  'api_key' => getenv('APIGEE_API_KEY') ?: '',
  'client_id' => getenv('APIGEE_CLIENT_ID') ?: '',
  'client_secret' => getenv('APIGEE_CLIENT_SECRET') ?: '',
  'token_path' => '/oauth/client_credential/accesstoken',
  'timeout' => 10,
];

For DDEV local development, set values in a local-only environment file or DDEV config. For production, inject secrets through your hosting platform, container secret store, or CI/CD deployment system.

Step 5: Create a Drupal Service

Create a small custom module, for example custom_apigee_client. Register a service instead of putting HTTP calls directly in controllers.

custom_apigee_client.services.yml

services:
  custom_apigee_client.client:
    class: Drupal\custom_apigee_client\ApigeeClient
    arguments:
      - '@http_client'
      - '@settings'
      - '@cache.default'
      - '@logger.channel.custom_apigee_client'

  logger.channel.custom_apigee_client:
    parent: logger.channel_base
    arguments: ['custom_apigee_client']

Step 6: Implement an API-Key Client

If the proxy uses VerifyAPIKey, Drupal can send the key in a header.

<?php

namespace Drupal\custom_apigee_client;

use Drupal\Component\Serialization\Json;
use Drupal\Core\Cache\CacheBackendInterface;
use Drupal\Core\Logger\LoggerChannelInterface;
use Drupal\Core\Site\Settings;
use GuzzleHttp\ClientInterface;
use GuzzleHttp\Exception\GuzzleException;

final class ApigeeClient {

  public function __construct(
    private readonly ClientInterface $httpClient,
    private readonly Settings $settings,
    private readonly CacheBackendInterface $cache,
    private readonly LoggerChannelInterface $logger,
  ) {}

  public function get(string $path, array $query = []): array {
    $config = $this->settings->get('apigee', []);
    $base_uri = rtrim((string) ($config['base_uri'] ?? ''), '/');
    $api_key = (string) ($config['api_key'] ?? '');

    if ($base_uri === '' || $api_key === '') {
      throw new \RuntimeException('Apigee base URI or API key is not configured.');
    }

    try {
      $response = $this->httpClient->request('GET', $base_uri . '/' . ltrim($path, '/'), [
        'headers' => [
          'Accept' => 'application/json',
          'x-apikey' => $api_key,
        ],
        'query' => $query,
        'timeout' => (float) ($config['timeout'] ?? 10),
      ]);

      return Json::decode((string) $response->getBody()) ?? [];
    }
    catch (GuzzleException $e) {
      $this->logger->error('Apigee request failed: @message', [
        '@message' => $e->getMessage(),
      ]);
      throw $e;
    }
  }

}

A controller or form submit handler can now call the service:

$result = $this->apigeeClient->get('/content/v1/articles', [
  'topic' => 'drupal',
  'limit' => 10,
]);

Step 7: Implement OAuth Client Credentials

With OAuth, Drupal should request a token, cache it until shortly before expiration, and then send it as a bearer token.

private function getAccessToken(): string {
  $cached = $this->cache->get('custom_apigee_client.oauth_token');
  if ($cached && is_string($cached->data)) {
    return $cached->data;
  }

  $config = $this->settings->get('apigee', []);
  $base_uri = rtrim((string) ($config['base_uri'] ?? ''), '/');
  $token_path = (string) ($config['token_path'] ?? '/oauth/client_credential/accesstoken');

  $response = $this->httpClient->request('POST', $base_uri . $token_path, [
    'auth' => [
      (string) $config['client_id'],
      (string) $config['client_secret'],
    ],
    'form_params' => [
      'grant_type' => 'client_credentials',
    ],
    'headers' => [
      'Accept' => 'application/json',
    ],
    'timeout' => (float) ($config['timeout'] ?? 10),
  ]);

  $payload = Json::decode((string) $response->getBody()) ?? [];
  $token = (string) ($payload['access_token'] ?? '');
  $expires_in = max(60, (int) ($payload['expires_in'] ?? 300));

  if ($token === '') {
    throw new \RuntimeException('Apigee token response did not include an access token.');
  }

  $this->cache->set(
    'custom_apigee_client.oauth_token',
    $token,
    time() + $expires_in - 30
  );

  return $token;
}

Then send the token on API calls:

$response = $this->httpClient->request('GET', $base_uri . '/content/v1/articles', [
  'headers' => [
    'Accept' => 'application/json',
    'Authorization' => 'Bearer ' . $this->getAccessToken(),
  ],
  'timeout' => 10,
]);

Step 8: Add Caching in Drupal

Do not call Apigee on every page request unless the data truly has to be real time. Use Drupal cache bins and cache metadata.

$cid = 'custom_apigee_client.articles.' . hash('sha256', serialize($query));
$cached = $this->cache->get($cid);

if ($cached) {
  return $cached->data;
}

$data = $this->get('/content/v1/articles', $query);
$this->cache->set($cid, $data, time() + 300, ['apigee:articles']);

return $data;

If the data appears in render arrays, add cache tags and a reasonable max-age. If editors need instant refresh, provide a cache clear action for the specific integration tags.

Step 9: Test the Apigee Proxy Before Drupal

Test from the command line before debugging Drupal.

curl -i \
  -H "Accept: application/json" \
  -H "x-apikey: $APIGEE_API_KEY" \
  "https://api.example.com/content/v1/articles?limit=1"

For OAuth:

curl -i \
  -u "$APIGEE_CLIENT_ID:$APIGEE_CLIENT_SECRET" \
  -d "grant_type=client_credentials" \
  "https://api.example.com/oauth/client_credential/accesstoken"

curl -i \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "Accept: application/json" \
  "https://api.example.com/content/v1/articles?limit=1"

Only wire Drupal after curl works. This saves a lot of time.

Step 10: Add Observability

Log enough information to troubleshoot without leaking secrets. Good fields include:

  • Apigee proxy path
  • HTTP method
  • Status code
  • Request duration
  • Drupal route or feature that made the call
  • Apigee correlation ID or response header, if your proxy provides one

Never log API keys, client secrets, bearer tokens, raw authorization headers, or sensitive response payloads.

Common Challenges and Fixes

1. 401 Unauthorized from Apigee

Usually this means the key or token is missing, expired, malformed, or read from the wrong header. Confirm the policy expects the same location Drupal uses. For API keys, compare request.header.x-apikey with the actual Drupal request header. For OAuth, confirm Drupal is sending Authorization: Bearer TOKEN.

2. 403 Forbidden Even Though the Key Is Valid

The developer app may not be approved for the API product, or the API product may not include the proxy/resource path Drupal is calling. In Apigee, check developer app status, product association, product resources, environment, and quota settings.

3. It Works in curl but Not in Drupal

Compare the full URL, headers, query string, and HTTP method. Drupal code often adds a leading slash incorrectly, double-encodes query values, omits Accept: application/json, or calls a different environment hostname than the one tested with curl.

4. Token Expiry Causes Random Failures

Cache OAuth tokens for slightly less than their lifetime. If the token expires in 300 seconds, cache it for about 270 seconds. On a 401 response, clear the token cache once and retry a single time. Avoid retry loops.

5. CORS Errors in the Browser

If Drupal calls Apigee server-side, CORS is not involved. CORS appears when browser JavaScript calls Apigee directly. For most Drupal website integrations, keep the Apigee call on the server and return Drupal-rendered output or a Drupal JSON endpoint to the browser.

6. SSL or Certificate Problems

Do not disable TLS verification in production. Fix the certificate chain, hostname, corporate proxy trust, or container CA bundle. In DDEV or containers, confirm the PHP/Guzzle runtime has the correct CA certificates installed.

7. Quota Exhaustion

If Apigee returns quota or rate-limit errors, add Drupal-side caching, reduce duplicate calls, debounce form-driven requests, and confirm the API product quota matches expected traffic. For high-traffic pages, avoid making Apigee calls during anonymous page rendering unless the result is cached.

8. Backend Errors Are Hard to Understand

Use Apigee fault rules to normalize backend errors into predictable JSON. Drupal should not have to parse five different backend error shapes. A consistent response such as { "error": "service_unavailable", "message": "Try again later" } makes Drupal rendering and logging much cleaner.

9. Secrets Leak Through Configuration

Never export real Apigee secrets into Drupal config sync. Keep secrets in environment variables, settings files excluded from Git, or a secret manager. Review logs and watchdog entries to make sure request options are not dumped during exceptions.

10. Local, Stage, and Production Point to the Wrong Apigee Environment

Use separate base URIs and developer apps for each environment. Name them clearly: drupal-local, drupal-stage, and drupal-prod. This avoids accidentally testing against production quotas or production data.

Deployment Checklist

  • Apigee proxy is deployed to the correct environment.
  • API product includes the proxy and resource paths Drupal needs.
  • Developer app is approved and associated with the API product.
  • Drupal secrets are injected through environment-specific settings.
  • curl works from the same network where Drupal runs.
  • Drupal service has timeouts, logging, and exception handling.
  • OAuth tokens and API responses are cached appropriately.
  • No credentials are logged or exposed to browser JavaScript.
  • Apigee quotas and Drupal cache strategy have been reviewed together.

Recommended Production Pattern

For most enterprise Drupal builds, the best long-term setup is:

  • Drupal calls Apigee server-side only.
  • OAuth 2.0 client credentials is used for production integrations.
  • API keys are reserved for lower-risk app identification or non-sensitive internal calls.
  • Drupal caches both tokens and read-heavy API responses.
  • Apigee owns traffic policies, quota, spike arrest, analytics, and normalized fault responses.
  • Drupal owns presentation, editorial workflow, render caching, and user experience.

References

Keep reading

Drupal Sep 7, 2026 6 min read

Build a Drupal Chatbot with Local AI

Learn how a Drupal chatbot finds relevant published articles, uses Ollama to generate answers, and displays source links while keeping inference on your own hardware.