Skip to content
Mehedi Hasan Sagor

// blog

Versioning, Rate Limiting, and Token Revocation: Harden Your Laravel APIs for Production

Mehedi Hasan Sagor Oct 12, 2026 5 min read
Versioning, Rate Limiting, and Token Revocation: Harden Your Laravel APIs for Production

Why API versioning matters

When a startup’s product gains traction, the API surface inevitably evolves. Adding fields, changing response formats, or deprecating endpoints can break existing client integrations. Versioning isolates change, giving you a predictable upgrade path and preventing downstream outages.

  • Predictability – Clients know exactly which contract they are consuming.
  • Rollback safety – If a new version introduces a bug, you can keep the old version alive while fixing it.
  • Team autonomy – Different squads can work on independent versions without stepping on each other's toes.

Laravel makes versioning straightforward through route groups. The key is to keep version‑specific logic out of the core business layer, so you can reuse services and models across versions.

Implementing versioned routes in Laravel

  1. Create a versioned route file – Place each version under routes/api/v1.php, routes/api/v2.php, etc.
  2. Group the routes – Apply a common prefix and namespace.
  3. Bind the version to a controller namespace – This keeps controllers tidy and avoids naming collisions.
// routes/api/v1.php
use Illuminate\Support\Facades\Route;

Route::prefix('v1')
    ->namespace('App\\Http\\Controllers\\Api\\V1')
    ->group(function () {
        Route::apiResource('posts', 'PostController');
        Route::get('stats', 'StatsController@summary');
    });
// routes/api/v2.php
use Illuminate\Support\Facades\Route;

Route::prefix('v2')
    ->namespace('App\\Http\\Controllers\\Api\\V2')
    ->group(function () {
        // New endpoint that replaces the old stats route
        Route::get('analytics', 'AnalyticsController@overview');
        // Same resource, but with a new response shape
        Route::apiResource('posts', 'PostController');
    });

Add the files to RouteServiceProvider:

// app/Providers/RouteServiceProvider.php
public function map()
{
    $this->mapApiRoutes();
    // other mappings …
}

protected function mapApiRoutes()
{
    foreach (glob(base_path('routes/api/*.php')) as $routeFile) {
        Route::middleware('api')
            ->prefix('api')
            ->group($routeFile);
    }
}

Best practice: Keep the controller logic thin. Move transformation (e.g., Fractal, Laravel Resources) into dedicated Transformer classes so the same service can serve multiple versions with different payloads.

Centralized rate limiting with Laravel's throttle middleware

Rate limiting protects your API from accidental overload and malicious abuse. Laravel ships with a flexible ThrottleRequests middleware that can be configured per route, per user, or globally.

Defining custom limits

Add a named limiter in App\Providers\RouteServiceProvider:

// app/Providers/RouteServiceProvider.php
use Illuminate\Cache\RateLimiting\Limit;
use Illuminate\Support\Facades\RateLimiter;

public function boot()
{
    $this->configureRateLimiting();
    parent::boot();
}

protected function configureRateLimiting()
{
    RateLimiter::for('api', function ($request) {
        // Authenticated users get 200 requests per minute, unauthenticated 60
        $maxAttempts = $request->user() ? 200 : 60;
        return Limit::perMinute($maxAttempts)->by($request->ip());
    });
}

Apply it to your routes:

Route::middleware(['auth:sanctum', 'throttle:api'])
    ->group(function () {
        Route::apiResource('orders', 'OrderController');
    });

Tiered limits for premium plans

If you expose tiered SaaS plans, create separate limiters:

RateLimiter::for('premium', function ($request) {
    return Limit::perMinute(1000)->by($request->user()->id);
});

Then assign the limiter based on the user’s subscription:

Route::middleware([
    'auth:sanctum',
    function ($request, $next) {
        $limiter = $request->user()->isPremium() ? 'premium' : 'api';
        return app('router')->middleware('throttle:' . $limiter)->handle($request, $next);
    }
])->group(function () {
    Route::apiResource('reports', 'ReportController');
});

Tip: Return the remaining quota in response headers (X-RateLimit-Remaining) so client libraries can adapt gracefully.

Secure token lifecycle: short‑lived access tokens and revocation

Most Laravel APIs use Laravel Sanctum or Passport for token‑based authentication. A common mistake is issuing long‑lived tokens and never providing a revocation path. This leaves the surface open if a token is leaked.

Adopt short‑lived access tokens

When using Sanctum’s SPA mode, you can combine it with Laravel’s built‑in personal_access_token model to issue JWT‑like tokens with an explicit expiry:

use Laravel\Sanctum\PersonalAccessToken;

public function createToken(User $user)
{
    $token = $user->createToken('api-token', ['*'], now()->addMinutes(30));
    return $token->plainTextToken;
}

The third argument (now()->addMinutes(30)) sets the expires_at column. Middleware checks this column and rejects expired tokens automatically.

Refresh tokens

Because access tokens are short‑lived, provide a refresh endpoint that issues a new token after verifying a long‑lived refresh token stored securely (e.g., HttpOnly cookie).

public function refresh(Request $request)
{
    $refreshToken = $request->cookie('refresh_token');
    if (! $refreshToken || ! $this->isValidRefreshToken($refreshToken)) {
        abort(401, 'Invalid refresh token');
    }

    $user = $this->userFromRefreshToken($refreshToken);
    $newAccess = $user->createToken('api-token', ['*'], now()->addMinutes(30));
    return response()->json(['access_token' => $newAccess->plainTextToken]);
}

Token revocation strategy

  1. Immediate revocation – When a user logs out or an admin revokes access, delete the token record:
    $request->user()->tokens()->where('name', 'api-token')->delete();
    
  2. Blacklist compromised tokens – Store a hash of revoked tokens in a fast store (Redis). The auth middleware checks the blacklist before authenticating.
  3. Grace period for rotation – When rotating keys, keep the old signing key valid for a short window (e.g., 5 minutes) to avoid abrupt failures.

Putting it together: a checklist for production‑ready Laravel APIs

  • Versioning
    • Create a separate route file per version.
    • Keep controllers thin; use transformers/resources for version‑specific payloads.
    • Document deprecation timelines clearly for clients.
  • Rate limiting
    • Define named limiters (api, premium, etc.) in RouteServiceProvider.
    • Attach throttle middleware per route group.
    • Return quota headers (X-RateLimit-Limit, X-RateLimit-Remaining).
  • Authentication & token management
    • Issue short‑lived access tokens (≤30 min).
    • Provide a refresh endpoint with HttpOnly cookies.
    • Implement immediate revocation and optional blacklist.
    • Rotate signing keys on a regular schedule.
  • Observability
    • Log version, endpoint, and user ID on every request.
    • Emit metrics for rate‑limit hits and token revocations.
    • Use Laravel Telescope or an external APM for tracing.

By applying these concrete steps, you turn a functional Laravel API into a resilient, secure, and client‑friendly service that can grow with your startup’s user base.

Actionable takeaway: Pick one of the three pillars—versioning, rate limiting, or token lifecycle—and implement it in the next sprint. You’ll immediately see fewer broken client integrations, fewer abusive traffic spikes, and a clearer security posture.

  • Laravel
  • Api
  • Security
  • Versioning
  • Bestpractices