API versioning allows you to make breaking changes without affecting existing API consumers. There are several strategies—let's explore the most common approaches.
URL Versioning (Most Common)
Version in the URL path:
/api/v1/posts
/api/v2/posts
Route Organization
php// routes/api.php // Version 1 Route::prefix('v1')->group(function () { Route::apiResource('posts', App\Http\Controllers\Api\V1\PostController::class); Route::apiResource('users', App\Http\Controllers\Api\V1\UserController::class); }); // Version 2 Route::prefix('v2')->group(function () { Route::apiResource('posts', App\Http\Controllers\Api\V2\PostController::class); Route::apiResource('users', App\Http\Controllers\Api\V2\UserController::class); });
Controller Organization
app/Http/Controllers/Api/
├── V1/
│ ├── PostController.php
│ └── UserController.php
├── V2/
│ ├── PostController.php
│ └── UserController.php
└── Controller.php
Version-Specific Resources
app/Http/Resources/
├── V1/
│ ├── PostResource.php
│ └── UserResource.php
└── V2/
├── PostResource.php
└── UserResource.php
Header Versioning
Version via Accept header:
Accept: application/vnd.myapi.v1+json
Accept: application/vnd.myapi.v2+json
Middleware Implementation
php<?php namespace App\Http\Middleware; use Closure; use Illuminate\Http\Request; class ApiVersion { public function handle(Request $request, Closure $next, string $version) { $accept = $request->header('Accept', ''); // Check for versioned media type if (preg_match('/application\/vnd\.myapi\.v(\d+)\+json/', $accept, $matches)) { $requestedVersion = $matches[1]; if ($requestedVersion !== $version) { return response()->json([ 'error' => 'API version mismatch', 'message' => "This endpoint requires v{$version}", ], 406); } } return $next($request); } }
Query Parameter Versioning
/api/posts?version=1
/api/posts?version=2
Less common, not recommended for production APIs.
Practical URL Versioning Setup
php// routes/api/v1.php <?php use App\Http\Controllers\Api\V1\PostController; use App\Http\Controllers\Api\V1\UserController; use App\Http\Controllers\Api\V1\CommentController; Route::apiResource('posts', PostController::class); Route::apiResource('users', UserController::class); Route::apiResource('posts.comments', CommentController::class)->shallow();
php// routes/api/v2.php <?php use App\Http\Controllers\Api\V2\PostController; use App\Http\Controllers\Api\V2\UserController; // V2 changes: posts have different response format Route::apiResource('posts', PostController::class); Route::apiResource('users', UserController::class); // New endpoint in V2 Route::get('posts/{post}/related', [PostController::class, 'related']);
php// routes/api.php <?php // Include version-specific routes Route::prefix('v1') ->middleware(['auth:sanctum', 'throttle:api']) ->group(base_path('routes/api/v1.php')); Route::prefix('v2') ->middleware(['auth:sanctum', 'throttle:api']) ->group(base_path('routes/api/v2.php')); // Default to latest stable version Route::middleware(['auth:sanctum', 'throttle:api']) ->group(base_path('routes/api/v2.php'));
Version-Specific Controllers
php// app/Http/Controllers/Api/V1/PostController.php namespace App\Http\Controllers\Api\V1; use App\Http\Controllers\Controller; use App\Http\Resources\V1\PostResource; use App\Models\Post; class PostController extends Controller { public function index() { return PostResource::collection(Post::paginate(15)); } public function show(Post $post) { return new PostResource($post); } }
php// app/Http/Controllers/Api/V2/PostController.php namespace App\Http\Controllers\Api\V2; use App\Http\Controllers\Controller; use App\Http\Resources\V2\PostResource; use App\Models\Post; class PostController extends Controller { public function index() { // V2: Always include author and comments count return PostResource::collection( Post::with('user')->withCount('comments')->paginate(15) ); } public function show(Post $post) { return new PostResource($post->load('user', 'tags', 'comments.user')); } // New in V2 public function related(Post $post) { $related = Post::where('category_id', $post->category_id) ->where('id', '!=', $post->id) ->limit(5) ->get(); return PostResource::collection($related); } }
Best Practices
- Support multiple versions during transition periods
- Document deprecation timelines clearly
- Version early - it's easier to add versioning from the start
- Don't over-version - minor changes don't need new versions
- Use semantic versioning for breaking changes only
php// Deprecation header public function index() { return PostResource::collection(Post::all()) ->response() ->header('X-API-Deprecation-Date', '2025-01-01') ->header('X-API-Deprecation-Info', 'Use /api/v2/posts instead'); }