How To Use Laravel As A Service Proxy Or API Gateway — 2 Powerful Approaches and Their Benefits

When you are building microservices, having a reliable way to connect to external services is essential. Whether you’re linking up with payment gateways, email providers, or internal tools that sit on different networks, a service proxy can handle these interactions. When setting up a proxy, you may think of heavy-duty API gateways like Traefik or Kong — but what if you don’t need the full suite of features they offer? Laravel can be an effective solution for creating a custom proxy service with minimal overhead.

In this tutorial, I’ll walk through building an API gateway or proxy endpoint in Laravel. If you’re already using Laravel in your stack, this can save you from introducing a separate tool and help you build custom proxy logic directly in your Laravel app.

Why?

Why Consider Laravel as a Proxy Service?

When your application needs to handle requests to multiple external services, a proxy can:

  1. Centralize and Secure API Credentials: By funneling API requests through a single Laravel proxy, you don’t need to expose credentials across multiple services. This setup is both secure and easy to manage.
  2. Share Resources Across Services: For instance, with OAuth tokens, each service can access a shared token managed by your Laravel proxy. You can even add caching for high-frequency resources to reduce load and latency.

Though Laravel isn’t typically seen as an API gateway, it’s well-suited for cases where you’re embedding a proxy within an existing Laravel app and need to add specific logic.

Benefits of Using Laravel Over a Dedicated API Gateway

  • Easily Customizable Logic: Dedicated API gateways may require complex scripting or plugins to achieve custom workflows. With Laravel, you can write custom proxy logic using familiar PHP, making it easy to maintain and expand.
  • Minimal Overhead: Adding Laravel as a proxy doesn’t require learning a new technology stack, which is great if you want to avoid extra maintenance.

Drawbacks to Keep in Mind

  • Performance Overhead: Running additional HTTP requests through Laravel could slow down response times in certain setups, although caching can mitigate this.
  • Scalability Limits: Laravel is not as scalable as dedicated gateways, so this approach is best for low to moderate traffic.

What is a Proxy Server?

proxy server is a layer that forwards incoming requests to appropriate backend services based on certain criteria, like URL segments. It acts as a basic router for requests without performing additional business logic. The primary role of a proxy server is to decouple the client from the underlying services, making it easier to manage different microservices.

For example, in a system with services for authentication (auth) and job listings (jobs), a proxy server can route requests to the corresponding service based on the first URL segment: /auth or /jobs.

Method 1: For multiple services

Requirements

For this tutorial, we’ll use the Guzzle HTTP client to manage outgoing HTTP requests. Laravel has a built-in HTTP client, but Guzzle offers added flexibility, which we’ll need.

First, install Guzzle in your Laravel project:

composer require guzzlehttp/guzzle

Setting Up Laravel as a Proxy Gateway

We’ll walk through the steps to configure Laravel to act as a proxy, forwarding requests to different services based on the URL’s first segment.

Step 1: Define Service Base URLs

Create a configuration file (config/services.php) to map each route prefix (like /auth or /orders) to its corresponding service base URL:

// config/services.php
return [
    'services' => [
        'auth' => 'https://auth-service-url.com',
        'orders' => 'https://order-service-url.com',
        'payment' => 'https://payment-service-url.com',
        'notification' => 'https://notification-service-url.com',
        // Add more services as needed
    ],
];

Step 2: Set Up a Dynamic Proxy Route

In your routes/api.php file, define a route that captures the first URL segment as the service name and the remaining path. This route will then forward the request to the correct service based on the configuration.

use GuzzleHttp\Client as HttpClient;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Config;

Route::any('/{service}/{path?}', function (Request $request, $service, $path = null) {
    // Load service URLs from config
    $services = Config::get('services.services');

    // Check if the requested service exists in the configuration
    if (!isset($services[$service])) {
        return response()->json(['error' => 'Service not found'], 404);
    }

    $client = new HttpClient(['base_uri' => $services[$service]]);

    try {
        // Set up the options for forwarding the request
        $options = [
            'headers' => $request->headers->all(),
            'query' => $request->query(),
            'json' => $request->all(),
        ];

        // Forward the request to the correct service and path
        $response = $client->request($request->method(), $path, $options);

        return response($response->getBody(), $response->getStatusCode())
                ->withHeaders($response->getHeaders());
    } catch (RequestException $e) {
        // Handle any exceptions from the request
        $statusCode = $e->hasResponse() ? $e->getResponse()->getStatusCode() : 500;
        $message = $e->getMessage();

        return response()->json(['error' => $message], $statusCode);
    }
})->where('path', '.*'); /// required to allow all following path segments

This setup allows Laravel to dynamically route incoming requests to different services based on the URL. When a request comes in, the route {service}/{path?} captures the first part of the URL (like auth or order) as the service name, and any additional parts as the path. Laravel then checks if the service exists in the configuration file. If it doesn’t, a 404 error is returned to indicate that the service wasn’t found.

If the service is valid, the request is forwarded using the Guzzle HTTP client. This means that all the original request details — such as headers, body, and any query parameters — are sent to the specified service URL. If something goes wrong while forwarding the request, the setup catches the error and sends back a relevant message and status code. This approach makes routing flexible and easy to maintain, while also providing error handling to inform users if something fails in the request process.

Method 2: For Single Service

Step 1: Set Up a Basic Proxy Route

Let’s create an endpoint that proxies requests to httpbin.org. In your routes/api.php file, add:

use GuzzleHttp\Client as HttpClient;

Route::any('/proxy/{path}', function(Request $request, $path) {
    $client = new HttpClient(['base_uri' => 'https://your-service-url.com']);

    return $client->request($request->method(), $path);
});

Handling Subpaths

The initial implementation only forwards the first part of the path (e.g., /proxy/get works, but /proxy/get/subpath doesn’t). To capture the full path, modify the route as follows:

Route::any('/proxy/{path}', function (Request $request, $path) {
    // Proxy code here...
})->where('path', '.*');

Adding where('path', '.*') ensures that the route can capture all subpaths.

Forwarding Request Body, Query Parameters, and Response Code

Our current setup doesn’t forward the request body, query parameters, or response status code. Let’s update the code to handle these aspects:

use GuzzleHttp\Exception\RequestException;

Route::any('/proxy/{path}', function (Request $request, $path) {
    $client = new HttpClient(['base_uri' => 'https://httpbin.org']);

    try {
        $response = $client->request($request->method(), $path, [
            'query' => $request->query(),
            'body' => $request->getContent(),
        ]);

        return response($response->getBody()->getContents(), $response->getStatusCode());
    } catch (RequestException $e) {
        return response()->json(['error' => 'Service unavailable'], 503);
    }
})->where('path', '.*');

This code now forwards the request body and query parameters, and it returns the response status code.

Filtering Headers

Forwarding headers can be tricky, as some headers can affect request validity or security. A good approach is to selectively forward only essential headers, like Content-Type and Accept. Here’s a helper function to filter headers:

function filterHeaders($headers) {
    $allowedHeaders = ['accept', 'content-type'];
    return array_filter($headers, function ($key) use ($allowedHeaders) {
        return in_array(strtolower($key), $allowedHeaders);
    }, ARRAY_FILTER_USE_KEY);
}

Then, use this helper in the endpoint to filter headers on both the request and response sides.

Final Code Example

Here’s the complete setup, including header filtering, request forwarding, and response handling:

use GuzzleHttp\Client as HttpClient;
use Illuminate\Http\Request;
use GuzzleHttp\Exception\RequestException;

// Helper function to filter headers
function filterHeaders($headers) {
    $allowedHeaders = ['accept', 'content-type'];
    return array_filter($headers, function ($key) use ($allowedHeaders) {
        return in_array(strtolower($key), $allowedHeaders);
    }, ARRAY_FILTER_USE_KEY);
}

Route::any('/proxy_example/{path}', function (Request $request, $path) {
    $client = new HttpClient([
        'base_uri' => 'https://httpbin.org', // Example API
        'timeout' => 60.0,
        'http_errors' => false, // Disable exceptions on 4xx/5xx responses
    ]);

    try {
        // Make request with filtered headers and body content
        $response = $client->request($request->method(), $path, [
            'headers' => filterHeaders($request->headers->all()),
            'query' => $request->query(),
            'body' => $request->getContent(),
        ]);

        // Forward the response back to the caller
        return response($response->getBody()->getContents(), $response->getStatusCode())
            ->withHeaders(filterHeaders($response->getHeaders()));
    } catch (RequestException $e) {
        return response()->json(['error' => 'Service unavailable'], 503);
    }
})->where('path', '.*');

This is a pretty proxy. This setup should cover the majority of basic proxy use cases. You can further extend it by adding authentication, caching mechanisms to reduce network requests, or even load balancing for more complex service structures.

Conclusion

Using Laravel as a proxy can simplify microservice communication without adding the complexity of a dedicated API gateway. While it may not match the performance of specialized tools, it offers flexibility and simplicity for many use cases, especially if you already have a Laravel stack.

By following this tutorial, you now have a basic service proxy in Laravel that you can further customize based on your application needs.

Keep building amazing software.