Skip to content

How to Implement API Versioning in Laravel 12

Learn API versioning in Laravel 12 with step-by-step examples, best practices, and pro tips to build scalable, future-ready REST APIs.

Dasun Kalupahana 1 min read 814 views 0 comments

API versioning is crucial for maintaining backward compatibility while evolving your application. In this guide, we’ll implement a clean, scalable API versioning system in Laravel 12 using route prefixes, middleware, and versioned controllers.

🚀 Step 1: Setting Up API Versioning in Laravel

1. Configure the Base API Routes (routes/api.php)

Instead of dumping all routes in one file, we split them into versioned route files.

<?php

// routes/api.php
use Illuminate\Support\Facades\Route;

Route::prefix('v1')->group(base_path('routes/api/v1.php'));
Route::prefix('v2')->group(base_path('routes/api/v2.php'));

2. Create Version-Specific Route Files

Store versioned routes in routes/api/v1.php and routes/api/v2.php.

Example (routes/api/v1.php):

<?php

use App\Http\Controllers\API\V1\UserController;
use Illuminate\Support\Facades\Route;

Route::apiResource('users', UserController::class);

🛠️ Step 2: Organizing Versioned Controllers

Keep controllers separated by version:

app/Http/Controllers/API/
├── V1/
│   ├── UserController.php
├── V2/
│   ├── UserController.php

🔧 Step 3: Adding API Version Middleware

To track the API version in use, create a middleware:

php artisan make:middleware ApiVersionMiddleware

Update the middleware (app/Http/Middleware/ApiVersionMiddleware.php):

<?php

namespace App\Http\Middleware;

use Closure;
use Illuminate\Http\Request;
use Symfony\Component\HttpFoundation\Response;

class ApiVersionMiddleware
{
    /**
     * Handle an incoming request.
     *
     * @param Request
     * @param Closure $next
     * @param string $version
     */
    public function handle(Request $request, Closure $next, string $version)
    {
        // Set the API version in config
        config(['app.api_version' => $version]);

        return $next($request)->header('X-API-Version', $version);
    }
}

Register it in bootstrap/app.php:

return Application::configure(basePath: dirname(__DIR__))
    ->withRouting(
        web: __DIR__.'/../routes/web.php',
        api: __DIR__.'/../routes/api.php',
        apiPrefix: 'api',
        commands: __DIR__.'/../routes/console.php',
        health: '/up',
    )
    ->withMiddleware(function (Middleware $middleware) {
        $middleware->alias([
            'api.version' => \App\Http\Middleware\ApiVersionMiddleware::class,
        ]);
    })
    ->create();

Apply middleware in routes (routes/api.php):

<?php

// routes/api.php
use Illuminate\Support\Facades\Route;

Route::prefix('v1')
    ->middleware('api.version:v1')
    ->group(base_path('routes/api/v1.php'));

Route::prefix('v2')
    ->middleware('api.version:v2')
    ->group(base_path('routes/api/v2.php'));

✅ Step 4: Testing Versioned APIs

<?php

namespace Tests\Feature\feature;

use Illuminate\Foundation\Testing\RefreshDatabase;
use Illuminate\Foundation\Testing\WithFaker;
use Tests\TestCase;

class AuthTest extends TestCase
{
    /**
     * A basic feature test example.
     */
    public function test_v1_login()
    {
        $response = $this->postJson('/api/v1/login', [
            'email' => 'test@example.com',
            'password' => 'test@example.com'
        ]);

        $response->assertStatus(200)
            ->assertHeader('X-API-Version', 'v1');
    }
}

To run a single test file:

//test file : AuthTest
//testsuite can be Feature or Unit
php artisan test --testsuite=Feature --filter=AuthTest

To run a specific test method:

php artisan test --testsuite=Feature --filter=test_v1_login

Test Result:

 PASS  Tests\Feature\feature\AuthTest
  ✓ v1 login                                                                                                                    2.70s  

  Tests:    1 passed (3 assertions)
  Duration: 2.92s

🚀 Final Thoughts & Best Practices

✔ Use Semantic Versioning (v1, v2) – Helps track breaking changes.
✔
Keep Controllers Thin – Delegate logic to services.
✔
Document API Changes – Use Swagger/OpenAPI for each version.

 

📌 Conclusion

By following this structured approach, you can maintain multiple API versions cleanly in Laravel 12. This ensures backward compatibility while allowing smooth transitions to newer versions.

💬 What’s your API versioning strategy? Let’s discuss in the comments!

If you found this helpful, feel free to share or drop a comment. Happy coding with Laravel! 🧱✨

Free resources for this topic

Code snippets and fixes for this topic

Get one practical tip every week

New tutorials, fixes and free cheat sheets from DEV Talk, straight to your inbox. No spam, unsubscribe in one click.

Free download: Laravel interview questions (PDF)

Have a question about this post?

Ask the community: other developers and the author can answer.

Ask the community

Get notified about new articles

One click, no email needed. Turn it off anytime with the bell at the top.

Get new articles on your iPhone

Add DEV Talk to your Home Screen, open it, then tap the bell.

Rate this post
0.0
Share

Enjoying the content?

If this article helped you, you can support the author with a coffee. It keeps new posts coming.

Buy me a coffee (opens in a new tab)

Comments 0

Be kind. Comments with links are reviewed before they appear.

No comments yet

Be the first to share your thoughts.

Get new articles in your inbox

Practical tutorials and deep dives, about once a week. No spam, unsubscribe anytime.

Privacy policy

Add DEV Talk to your Home Screen

  1. 1 Tap the Share button in Safari's toolbar.
  2. 2 Scroll down and tap Add to Home Screen.
  3. 3 Tap Add. Open it from the Home Screen to read like an app and to turn on notifications with the bell.

We use cookies to improve your experience and analyze traffic. By continuing, you agree to our Privacy Policy.