shahghasiadil / laravel-api-versioning
Attribute-based API versioning for Laravel with strict type safety
Fund package maintenance!
:vendor_name
Requires
- php: ^8.2
- laravel/framework: ^12.0
Requires (Dev)
- larastan/larastan: ^2.9||^3.0
- laravel/pint: ^1.14
- nunomaduro/collision: ^8.1.1||^7.10.0
- orchestra/testbench: ^10.0
- phpstan/extension-installer: ^1.3
- phpstan/phpstan-deprecation-rules: ^1.1||^2.0
- phpstan/phpstan-phpunit: ^1.3||^2.0
- phpstan/phpstan-strict-rules: ^1.5||^2.0
- phpunit/phpunit: ^11.0
README
A powerful and elegant attribute-based API versioning solution for Laravel applications with strict type safety and comprehensive deprecation management.
Features
- ๐ฏ Attribute-based versioning - Use PHP 8+ attributes to define API versions
- ๐ก๏ธ Type-safe - Full type annotations and strict type checking
- ๐ Multiple detection methods - Header, query parameter, path, and media type detection
- ๐ฆ Resource versioning - Smart version-aware JSON resources
- ๐ซ Deprecation support - Built-in deprecation warnings and sunset dates
- ๐ Version inheritance - Fallback chains for backward compatibility
- ๐งช Testing utilities - Comprehensive test helpers
- ๐ Route inspection - Commands to analyze your API versioning
- โก Performance optimized - Minimal overhead with efficient resolution
Requirements
- PHP 8.2+
- Laravel 12.0+
Installation
Install the package via Composer:
composer require shahghasiadil/laravel-api-versioning
Publish the configuration file:
php artisan vendor:publish --provider="ShahGhasiAdil\LaravelApiVersioning\ApiVersioningServiceProvider" --tag="config"
Quick Start
1. Configure Your API Versions
Update config/api-versioning.php
:
return [ 'default_version' => '2.0', 'supported_versions' => ['1.0', '1.1', '2.0', '2.1'], 'detection_methods' => [ 'header' => [ 'enabled' => true, 'header_name' => 'X-API-Version', ], 'query' => [ 'enabled' => true, 'parameter_name' => 'api-version', ], 'path' => [ 'enabled' => true, 'prefix' => 'api/v', ], ], 'version_method_mapping' => [ '1.0' => 'toArrayV1', '1.1' => 'toArrayV11', '2.0' => 'toArrayV2', '2.1' => 'toArrayV21', ], 'version_inheritance' => [ '1.1' => '1.0', // v1.1 falls back to v1.0 '2.1' => '2.0', // v2.1 falls back to v2.0 ], ];
2. Add Middleware to Routes
// routes/api.php - Using middleware alias Route::middleware('api.version')->group(function () { Route::apiResource('users', UserController::class); }); // Alternative: Using direct middleware class use ShahGhasiAdil\LaravelApiVersioning\Middleware\AttributeApiVersionMiddleware; Route::middleware(AttributeApiVersionMiddleware::class)->group(function () { Route::apiResource('users', UserController::class); }); // For specific routes only Route::apiResource('users', UserController::class)->middleware('api.version');
3. Create Versioned Controllers
Use the built-in command to generate versioned controllers:
# Create a basic versioned controller php artisan make:versioned-controller UserController --api-version=2.0 # Create a deprecated controller php artisan make:versioned-controller V1UserController --api-version=1.0 --deprecated --sunset=2025-12-31 --replaced-by=2.0
Or create manually with attributes:
<?php use ShahGhasiAdil\LaravelApiVersioning\Attributes\ApiVersion; use ShahGhasiAdil\LaravelApiVersioning\Attributes\Deprecated; use ShahGhasiAdil\LaravelApiVersioning\Traits\HasApiVersionAttributes; #[ApiVersion(['2.0', '2.1'])] class UserController extends Controller { use HasApiVersionAttributes; public function index(): JsonResponse { return response()->json([ 'data' => User::all(), 'version' => $this->getCurrentApiVersion(), 'deprecated' => $this->isVersionDeprecated(), ]); } }
4. Create Versioned Resources
<?php use ShahGhasiAdil\LaravelApiVersioning\Http\Resources\VersionedJsonResource; class UserResource extends VersionedJsonResource { protected function toArrayV1(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, ]; } protected function toArrayV2(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'created_at' => $this->created_at->toISOString(), ]; } protected function toArrayDefault(Request $request): array { return $this->toArrayV2($request); } }
Middleware Configuration
The package registers the middleware alias automatically. You have several options for applying it:
Option 1: Route Group (Recommended)
// routes/api.php Route::middleware('api.version')->group(function () { Route::apiResource('users', UserController::class); Route::apiResource('posts', PostController::class); });
Option 2: Direct Middleware Class
// routes/api.php use ShahGhasiAdil\LaravelApiVersioning\Middleware\AttributeApiVersionMiddleware; Route::middleware(AttributeApiVersionMiddleware::class)->group(function () { Route::apiResource('users', UserController::class); });
Option 3: Global Middleware (Laravel 12)
// bootstrap/app.php use ShahGhasiAdil\LaravelApiVersioning\Middleware\AttributeApiVersionMiddleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', apiMiddleware: [ 'throttle:api', \Illuminate\Routing\Middleware\SubstituteBindings::class, AttributeApiVersionMiddleware::class, // Add here for all API routes ], );
Option 4: Individual Routes
Route::middleware('api.version')->get('/users', [UserController::class, 'index']); Route::post('/users', [UserController::class, 'store'])->middleware('api.version');
Usage
API Version Detection
The package supports multiple ways to specify API versions:
Header-based (Recommended)
curl -H "X-API-Version: 2.0" https://api.example.com/users
Query Parameter
curl https://api.example.com/users?api-version=2.0
Path-based
curl https://api.example.com/api/v2.0/users
Media Type
curl -H "Accept: application/vnd.api+json;version=2.0" https://api.example.com/users
Attributes Reference
#[ApiVersion]
- Define Supported Versions
// Single version #[ApiVersion('2.0')] class UserController extends Controller {} // Multiple versions #[ApiVersion(['1.0', '1.1', '2.0'])] class UserController extends Controller {} // Method-specific versions class UserController extends Controller { #[ApiVersion('2.0')] public function store() {} }
#[ApiVersionNeutral]
- Version-Independent Endpoints
#[ApiVersionNeutral] class HealthController extends Controller { public function check() {} // Works with any version }
#[Deprecated]
- Mark as Deprecated
#[ApiVersion('1.0')] #[Deprecated( message: 'This endpoint is deprecated. Use v2.0 instead.', sunsetDate: '2025-12-31', replacedBy: '2.0' )] class V1UserController extends Controller {}
#[MapToApiVersion]
- Method-Specific Mapping
class UserController extends Controller { #[MapToApiVersion(['1.1', '2.0'])] public function show() {} // Only available in v1.1 and v2.0 }
Resource Versioning
Method-based Versioning
class UserResource extends VersionedJsonResource { protected function toArrayV1(Request $request): array { return ['id' => $this->id, 'name' => $this->name]; } protected function toArrayV2(Request $request): array { return [ 'id' => $this->id, 'name' => $this->name, 'email' => $this->email, 'profile' => ['avatar' => $this->avatar_url], ]; } protected function toArrayDefault(Request $request): array { return $this->toArrayV2($request); } }
Dynamic Configuration Versioning
class UserResource extends VersionedJsonResource { protected array $versionConfigs = [ '1.0' => ['id', 'name'], '2.0' => ['id', 'name', 'email', 'profile'], ]; protected function toArrayDefault(Request $request): array { $version = $this->getCurrentApiVersion(); $config = $this->versionConfigs[$version] ?? $this->versionConfigs['2.0']; return $this->only($config); } }
Helper Trait Methods
The HasApiVersionAttributes
trait provides useful methods:
class UserController extends Controller { use HasApiVersionAttributes; public function index() { $version = $this->getCurrentApiVersion(); // '2.0' $isDeprecated = $this->isVersionDeprecated(); // false $isNeutral = $this->isVersionNeutral(); // false $message = $this->getDeprecationMessage(); // null $sunset = $this->getSunsetDate(); // null $replacedBy = $this->getReplacedByVersion(); // null } }
Commands
Generate Versioned Controllers
# Basic controller php artisan make:versioned-controller UserController --api-version=2.0 # Deprecated controller php artisan make:versioned-controller V1UserController \ --api-version=1.0 \ --deprecated \ --sunset=2025-12-31 \ --replaced-by=2.0
Inspect API Versions
# Show all routes with version info php artisan api:versions # Filter by route pattern php artisan api:versions --route=users # Show only deprecated endpoints php artisan api:versions --deprecated # Filter by specific version php artisan api:versions --api-version=2.0
Manage Configuration
# Show current configuration php artisan api:version-config --show # Add new version mapping (guidance only) php artisan api:version-config --add-version=3.0 --method=toArrayV3
Testing
The package includes comprehensive testing utilities:
use ShahGhasiAdil\LaravelApiVersioning\Testing\ApiVersionTestCase; class UserControllerTest extends ApiVersionTestCase { public function test_user_endpoint_v1() { $response = $this->getWithVersion('/api/users', '1.0'); $response->assertOk(); $this->assertApiVersion($response, '1.0'); $this->assertApiVersionNotDeprecated($response); } public function test_deprecated_endpoint() { $response = $this->getWithVersion('/api/v1/users', '1.0'); $this->assertApiVersionDeprecated($response, '2025-12-31'); $this->assertDeprecationMessage($response, 'Use v2.0 instead'); $this->assertReplacedBy($response, '2.0'); } public function test_unsupported_version() { $response = $this->getWithVersion('/api/users', '3.0'); $response->assertStatus(400); $response->assertJson([ 'error' => 'Unsupported API Version', 'requested_version' => '3.0', ]); } }
Available Test Methods
getWithVersion($uri, $version, $headers = [])
getWithVersionQuery($uri, $version, $headers = [])
postWithVersion($uri, $data, $version, $headers = [])
putWithVersion($uri, $data, $version, $headers = [])
deleteWithVersion($uri, $version, $headers = [])
assertApiVersion($response, $expectedVersion)
assertApiVersionDeprecated($response, $sunsetDate = null)
assertApiVersionNotDeprecated($response)
assertSupportedVersions($response, $versions)
assertRouteVersions($response, $versions)
assertDeprecationMessage($response, $message)
assertReplacedBy($response, $version)
Response Headers
The middleware automatically adds helpful headers to API responses:
X-API-Version: 2.0 X-API-Supported-Versions: 1.0, 1.1, 2.0, 2.1 X-API-Route-Versions: 2.0, 2.1 X-API-Deprecated: true X-API-Deprecation-Message: This endpoint is deprecated X-API-Sunset: 2025-12-31 X-API-Replaced-By: 2.0
Error Handling
When an unsupported version is requested, the package returns a structured error response:
{ "error": "Unsupported API Version", "message": "API version '3.0' is not supported for this endpoint.", "requested_version": "3.0", "supported_versions": ["1.0", "1.1", "2.0", "2.1"], "endpoint_versions": ["2.0", "2.1"], "documentation": "https://docs.example.com/api" }
Advanced Examples
Complex Controller with Multiple Versions
<?php use ShahGhasiAdil\LaravelApiVersioning\Attributes\{ApiVersion, Deprecated, MapToApiVersion}; #[ApiVersion(['1.0', '1.1', '2.0'])] class UserController extends Controller { use HasApiVersionAttributes; public function index(): JsonResponse { $users = User::all(); return UserResource::collection($users); } #[MapToApiVersion(['2.0'])] public function store(Request $request): JsonResponse { // Only available in v2.0 $user = User::create($request->validated()); return new UserResource($user); } #[Deprecated(message: 'Use POST /users instead', replacedBy: '2.0')] #[MapToApiVersion(['1.0', '1.1'])] public function create(Request $request): JsonResponse { // Deprecated method for v1.x return $this->store($request); } }
Dynamic Resource Configuration
class UserResource extends VersionedJsonResource { protected array $versionConfigs = [ '1.0' => ['id', 'name'], '1.1' => ['id', 'name', 'email'], '2.0' => ['id', 'name', 'email', 'created_at', 'profile'], '2.1' => ['id', 'name', 'email', 'created_at', 'updated_at', 'profile', 'preferences', 'stats'], ]; protected function toArrayDefault(Request $request): array { $version = $this->getCurrentApiVersion(); $config = $this->versionConfigs[$version] ?? $this->versionConfigs['2.1']; $data = []; foreach ($config as $field) { $data[$field] = $this->getFieldValue($field); } return $data; } private function getFieldValue(string $field): mixed { return match($field) { 'profile' => ['avatar' => $this->avatar_url, 'bio' => $this->bio], 'preferences' => ['theme' => $this->theme ?? 'light'], 'stats' => ['login_count' => $this->login_count ?? 0], default => $this->$field, }; } }
Version-Neutral Endpoints
#[ApiVersionNeutral] class HealthController extends Controller { use HasApiVersionAttributes; public function check(): JsonResponse { return response()->json([ 'status' => 'healthy', 'timestamp' => now()->toISOString(), 'version' => $this->getCurrentApiVersion(), ]); } }
Configuration Options
Detection Methods
Configure how versions are detected from requests:
'detection_methods' => [ 'header' => [ 'enabled' => true, 'header_name' => 'X-API-Version', ], 'query' => [ 'enabled' => true, 'parameter_name' => 'api-version', ], 'path' => [ 'enabled' => true, 'prefix' => 'api/v', // Matches /api/v1.0/users ], 'media_type' => [ 'enabled' => false, 'format' => 'application/vnd.api+json;version=%s', ], ],
Version Inheritance
Set up fallback chains for backward compatibility:
'version_inheritance' => [ '1.1' => '1.0', // v1.1 falls back to v1.0 methods '1.2' => '1.1', // v1.2 falls back to v1.1, then v1.0 '2.1' => '2.0', // v2.1 falls back to v2.0 methods ],
Method Mapping
Map versions to specific resource methods:
'version_method_mapping' => [ '1.0' => 'toArrayV1', '1.1' => 'toArrayV11', '2.0' => 'toArrayV2', '2.1' => 'toArrayV21', ],
Artisan Commands
php artisan make:versioned-controller
Generate a new versioned controller with proper attributes:
# Basic usage php artisan make:versioned-controller ProductController --api-version=2.0 # With deprecation php artisan make:versioned-controller V1ProductController \ --api-version=1.0 \ --deprecated \ --sunset=2025-06-30 \ --replaced-by=2.0
Options:
--api-version=X.X
- Specify the API version (default: 1.0)--deprecated
- Mark the controller as deprecated--sunset=DATE
- Set sunset date for deprecated controller--replaced-by=VERSION
- Specify replacement version
php artisan api:versions
Display comprehensive versioning information for all API routes:
# Show all API routes php artisan api:versions # Filter by route pattern php artisan api:versions --route=users # Show only deprecated endpoints php artisan api:versions --deprecated # Filter by specific version php artisan api:versions --api-version=2.0
Options:
--route=PATTERN
- Filter routes by URI pattern--api-version=X.X
- Show only routes supporting specific version--deprecated
- Show only deprecated endpoints
php artisan api:version-config
Manage version configuration:
# Show current configuration php artisan api:version-config --show # Get guidance for adding new versions php artisan api:version-config --add-version=3.0 --method=toArrayV3
Options:
--show
- Display current version configuration--add-version=X.X
- Get instructions for adding new version--method=NAME
- Specify method name for new version
Best Practices
1. Version Naming Convention
Use semantic versioning (e.g., 1.0
, 1.1
, 2.0
) for clarity and consistency.
2. Backward Compatibility
Leverage version inheritance to maintain backward compatibility:
'version_inheritance' => [ '1.1' => '1.0', // v1.1 can fall back to v1.0 methods ],
3. Deprecation Strategy
Always provide clear deprecation information:
#[Deprecated( message: 'This endpoint will be removed in v3.0. Use /api/v2/users instead.', sunsetDate: '2025-12-31', replacedBy: '2.0' )]
4. Resource Organization
Keep version-specific logic organized in your resources:
// Good: Clear method names protected function toArrayV1(Request $request): array {} protected function toArrayV2(Request $request): array {} // Good: Inheritance for similar versions protected function toArrayV11(Request $request): array { return array_merge($this->toArrayV1($request), [ 'email' => $this->email, // Added field ]); }
5. Testing Strategy
Test all supported versions thoroughly:
public function test_all_supported_versions() { foreach (['1.0', '1.1', '2.0'] as $version) { $response = $this->getWithVersion('/api/users', $version); $response->assertOk(); $this->assertApiVersion($response, $version); } }
Error Responses
Unsupported Version
When a client requests an unsupported version:
Request:
curl -H "X-API-Version: 3.0" https://api.example.com/users
Response:
{ "error": "Unsupported API Version", "message": "API version '3.0' is not supported for this endpoint.", "requested_version": "3.0", "supported_versions": ["1.0", "1.1", "2.0", "2.1"], "endpoint_versions": ["2.0", "2.1"], "documentation": "https://docs.example.com/api" }
Migration Guide
From Other Versioning Solutions
- Install the package and publish the configuration
- Update your routes to use the
api.version
middleware - Add attributes to your existing controllers
- Migrate resources to extend
VersionedJsonResource
- Test thoroughly using the provided test utilities
Adding New Versions
-
Update configuration:
'supported_versions' => ['1.0', '1.1', '2.0', '2.1', '3.0'], 'version_method_mapping' => [ // ... existing mappings '3.0' => 'toArrayV3', ],
-
Add version to controllers:
#[ApiVersion(['2.0', '2.1', '3.0'])] class UserController extends Controller {}
-
Implement resource methods:
protected function toArrayV3(Request $request): array { // New version implementation }
-
Test the new version thoroughly
Contributing
Contributions are welcome! Please ensure you:
- Follow PSR-12 coding standards
- Add tests for new features
- Update documentation for changes
- Run the test suite:
composer test
- Run static analysis:
composer analyse
- Format code:
composer format
Security
If you discover any security-related issues, please email adil.shahghasi@gmail.com instead of using the issue tracker.
Credits
License
The MIT License (MIT). Please see License File for more information.
Changelog
Please see CHANGELOG for more information on what has changed recently.
Made with โค๏ธ for the Laravel community