Laravel Packages

Passage: A Lightweight API Proxy Gateway for Laravel

Published
Passage: A Lightweight API Proxy Gateway for Laravel image

Passage is a Laravel package by Morcen Chavez that lets your app sit between a client and an external API, forwarding requests and responses while keeping full control over authentication, headers, and payload transformation — all through familiar Laravel routing and middleware.

The typical use case is when you need to call a third-party API from your frontend but don't want to expose API keys, need to normalize payloads, or want to enforce validation and auth logic in one place. Instead of building a custom proxy from scratch, Passage gives you a structured way to do it with minimal boilerplate.

Defining Routes

Routes are registered using the Passage facade, right alongside your regular routes. The {path?} wildcard captures any sub-path and forwards it upstream:

use Morcen\Passage\Facades\Passage;
 
Passage::get('github/{path?}', GithubPassageController::class);
Passage::post('stripe/{path?}', StripePassageController::class);
Passage::any('payments/{path?}', PaymentsPassageController::class);

These support the standard HTTP methods — get, post, put, patch, delete, and any.

Creating a Handler

Each route points to a handler class that controls how requests are forwarded and responses are returned. Generate one with:

php artisan passage:controller GithubPassageController

Handlers extend PassageHandler and can implement three methods:

  • getOptions() — sets the upstream base URI and Guzzle options (timeouts, headers, etc.)
  • getRequest() — transforms or injects credentials into the outgoing request
  • getResponse() — transforms the upstream response before it reaches the client

A minimal handler might look like this:

class GithubPassageController extends PassageHandler
{
public function getOptions(): array
{
return [
'base_uri' => 'https://api.github.com/',
];
}
 
public function getRequest(Request $request): Request
{
return $this->withBearerToken($request, config('services.github.token'));
}
}

Note the trailing slash on base_uri — it's required for path forwarding to work correctly.

Built-in Authentication Helpers

Passage ships with three authentication traits you can use inside getRequest():

  • Bearer token$this->withBearerToken($request, $token)
  • API key (as a header or query param) — $this->withApiKey($request, $key) or $this->withApiKeyQuery($request, $key, 'api_key')
  • HMAC signing$this->withHmacSignature($request, $secret)

You can also scaffold a handler with auth pre-wired:

php artisan passage:controller StripePassageController --with-auth=apikey
php artisan passage:controller PaymentsPassageController --with-auth=hmac

Security

Passage automatically strips sensitive client headers — cookies, authorization, proxy-authorization — before forwarding requests upstream. If you need to selectively pass certain headers through (like forwarding the client's Authorization header), have your handler implement the AcceptsClientHeaders interface and define the allowedClientHeaders() method to return an allowlist:

class GithubPassageController extends PassageHandler implements AcceptsClientHeaders
{
public function allowedClientHeaders(): array
{
return ['authorization'];
}
}

You can also restrict which hosts can be proxied by setting PASSAGE_ENFORCE_ALLOWED_HOSTS=true in your environment.

Inbound Validation and Resilience

Handlers can validate incoming requests before they ever reach the upstream service by implementing ValidatesInboundRequest. Define Laravel validation rules in a rules() method, and any failures return a 422 — no upstream call is made.

use Morcen\Passage\Contracts\ValidatesInboundRequest;
 
class StripePassageController extends PassageHandler implements ValidatesInboundRequest
{
public function getOptions(): array
{
return ['base_uri' => 'https://api.stripe.com/'];
}
 
public function rules(): array
{
return [
'amount' => ['required', 'integer', 'min:1'],
'currency' => ['required', 'string', 'size:3'],
];
}
}

For resilience, withRetry() adds automatic retry with exponential backoff:

class PaymentsPassageController extends PassageHandler
{
public function getOptions(): array
{
return array_merge(
['base_uri' => 'https://payments.example.com/'],
$this->withRetry(3, 200), // 3 retries, 200ms initial delay
);
}
}

Passage also supports response caching for GET/HEAD routes and streaming responses for large payloads.

Useful Artisan Commands

  • php artisan passage:list — lists all registered proxy routes
  • php artisan passage:health — checks connectivity to upstream services

You can also disable all proxying without touching your routes by setting PASSAGE_ENABLED=false in your .env.


Passage is a clean solution for scenarios where you need a lightweight API proxy inside an existing Laravel app without reaching for a full enterprise gateway. Learn more and explore the source code on GitHub.

Yannick Lyn Fatt photo

Staff Writer at Laravel News and Full stack web developer.

Sponsored

acquaintsoft logo
Acquaint Softtech

Hire Laravel developers with AI expertise at $20/hr. Get started in 48 hours.

Visit Acquaint Softtech

The latest

View all →
CPX: The Composer Package Executor for PHP image

CPX: The Composer Package Executor for PHP

Read article
Laravel AI SDK Adds Human-in-the-Loop Tool Approval image

Laravel AI SDK Adds Human-in-the-Loop Tool Approval

Read article
Pest 5 Released With Test Impact Analysis, Agent Verification, and Evals image

Pest 5 Released With Test Impact Analysis, Agent Verification, and Evals

Read article
Queue-SQL: Run Mass Deletes and Updates Across Parallel Queue Jobs image

Queue-SQL: Run Mass Deletes and Updates Across Parallel Queue Jobs

Read article
Blade Formatting in Laravel Pint image

Blade Formatting in Laravel Pint

Read article
Inertia DevTools Is Now on the Chrome Web Store image

Inertia DevTools Is Now on the Chrome Web Store

Read article