Master API versioning and maintain backward compatibility in your distributed systems. Learn to implement header-based versioning for clean, scalable APIs.
Previously in this course, we explored distributed locks to manage state across high-concurrency environments. While locking ensures data integrity, evolving your API structure without breaking existing integrations is the next frontier of maintainability. This lesson adds a layer of architectural discipline by implementing header-based versioning to support multiple API versions side-by-side.
In high-traffic systems, your API is a contract. Breaking that contract forces your consumers into expensive migration cycles. While URI versioning (e.g., /api/v1/users) is standard, it often litters your routing logic and makes resource discovery (HATEOAS) more complex.
Header-based versioning—specifically using the Accept header—allows your URIs to remain clean and resource-oriented. It treats the version as a representation of the resource rather than a location.
| Strategy | Pros | Cons |
|---|---|---|
| URI Path | Visible, easy to cache, simple. | Changes resource identity, breaks REST principles. |
| Header | Clean URIs, content negotiation. | Harder to test in browsers, complex proxy caching. |
| Query Param | Easy to implement. | Overwrites cache keys, less standard. |
Laravel’s router is powerful, but it doesn't natively support "versioning" as a first-class citizen. We achieve this by creating a custom Route Matcher.
First, define a custom header, such as X-API-Version, or use the Accept header with a custom vendor type (e.g., application/vnd.myapp.v1+json). We will use the latter for better standards compliance.
We need a way to tell Laravel to route to a specific group based on the header. Create a middleware or a custom route macro. A cleaner approach for advanced architectures is using a custom Route condition.
PHP#6A9955">// app/Providers/RouteServiceProvider.php use Illuminate\Support\Facades\Route; use Illuminate\Http\Request; public function boot() { Route::macro('version', function ($version, $callback) { Route::group(['middleware' => "api.version:{$version}"], $callback); }); }
The middleware verifies the client's requested version. If the version is missing or unsupported, we can throw a 406 Not Acceptable error or default to a legacy version.
PHP#6A9955">// app/Http/Middleware/EnsureApiVersion.php namespace App\Http\Middleware; use Closure; use Symfony\Component\HttpKernel\Exception\NotAcceptableHttpException; class EnsureApiVersion { public function handle($request, Closure $next, $version) { $requested = $request->header('Accept'); if (!str_contains($requested, "vnd.myapp.{$version}+json")) { throw new NotAcceptableHttpException("Unsupported API version."); } return $next($request); } }
With the infrastructure in place, your api.php file remains readable even as your system grows.
PHP#6A9955">// routes/api.php Route::version('v1', function () { Route::get('/users', [App\Http\Controllers\V1\UserController::class, 'index']); }); Route::version('v2', function () { Route::get('/users', [App\Http\Controllers\V2\UserController::class, 'index']); });
As you scale, avoid "fat" controllers. Since we are already using Action Classes, your versioned controllers should strictly act as entry points that resolve the correct Domain Actions.
If v2 introduces a new field for the User object, your CreateUserAction might change. Use Data Transfer Objects (DTOs) to map different input formats into a unified Domain Object that your internal services understand.
EnsureApiVersion middleware as shown above.UserProfileController.V1, return the user's name. In V2, return an object containing first_name and last_name.curl -H "Accept: application/vnd.myapp.v2+json" http://your-app.test/api/users to verify the response changes dynamically.Vary: Accept header is sent. Otherwise, a v1 request might be cached and served to a v2 client.We've moved beyond simple routing by implementing a robust, header-driven versioning strategy. This ensures that our Modular Monolith Structure remains flexible, allowing us to evolve domain logic for new clients while keeping legacy integrations functional. By decoupling the version from the URI, we maintain cleaner, more RESTful interfaces.
Up next: We will tackle Database Migration Strategies, where we'll learn how to apply breaking schema changes without downtime or breaking existing API versions.
Learn how to use Laravel API resources to transform model data and return consistent, clean JSON responses for your RESTful applications.
Read moreMaster stateless API authentication in Laravel. Learn to issue and verify JWTs, implement secure token rotation, and handle revocation in a high-traffic system.
API Versioning Strategies
Custom Middleware Development
Database Connection Pooling
Handling Large Data Exports
Security Header Configuration
Database Sharding Concepts
Real-time Data Synchronization
Database Deadlock Prevention
Managing Third-Party API Integrations