grazulex / laravel-apiroute
Complete API versioning lifecycle management for Laravel
Requires
- php: ^8.3
- illuminate/contracts: ^12.0|^13.0
- illuminate/http: ^12.0|^13.0
- illuminate/routing: ^12.0|^13.0
- illuminate/support: ^12.0|^13.0
- nesbot/carbon: ^3.10
Requires (Dev)
- larastan/larastan: ^3.4
- laravel/pint: ^1.22
- orchestra/testbench: ^10.2|^11.0
- pestphp/pest: ^3.8|^4.0
- pestphp/pest-plugin-laravel: ^3.2|^4.0
- phpstan/phpstan: ^2.1
- rector/rector: ^2.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
This package is auto-updated.
Last update: 2026-09-18 16:12:57 UTC
README
Tip
What Laravel ApiRoute does for you โ Ship v2 of your API without breaking the clients still on v1. Version your routes, deprecate endpoints with proper HTTP headers and retire old versions on a schedule โ instead of hand-rolling all of this on every project.
This package is free and maintained on my own time. If it saves you hours, a small contribution helps me keep it going: ๐ GitHub Sponsors ยท โ Buy Me a Coffee ยท PayPal
Complete API versioning lifecycle management for Laravel
Features
- Multi-strategy versioning - URI path, Header, Query parameter, or Accept header
- Automatic deprecation headers -
Deprecation(RFC 9745) andSunset(RFC 8594) headers as RFC 7231 HTTP-dates - Version lifecycle management - Active, Deprecated, Sunset, Removed states
- Intelligent fallback - Route fallback to previous versions when needed
- Artisan commands - Scaffold, monitor, and manage API versions
- Usage tracking - Optional analytics per API version
- Zero configuration start - Works out of the box with sensible defaults
- Endpoint deprecation -
#[Deprecated]attribute or->deprecated()route macro withDeprecation(RFC 9745) andSunset(RFC 8594) headers and 410 sunset policy - JSON:API - error documents by content negotiation, version metadata for Laravel 13 JSON:API resources
Requirements
- PHP 8.3+
- Laravel 12.x or 13.x
Installation
composer require grazulex/laravel-apiroute
Publish the configuration file:
php artisan vendor:publish --tag="apiroute-config"
Documentation
For complete documentation including migrations, advanced configuration, and usage tracking setup, please visit the Wiki.
Quick Start
1. Define versions in config
// config/apiroute.php 'versions' => [ 'v1' => [ 'routes' => base_path('routes/api/v1.php'), 'status' => 'deprecated', 'deprecated_at' => '2025-06-01', 'sunset_at' => '2025-12-01', 'successor' => 'v2', ], 'v2' => [ 'routes' => base_path('routes/api/v2.php'), 'status' => 'active', ], 'v3' => [ 'routes' => base_path('routes/api/v3.php'), 'status' => 'beta', ], ],
2. Create route files
// routes/api/v2.php use Illuminate\Support\Facades\Route; Route::apiResource('users', App\Http\Controllers\Api\V2\UserController::class);
Versioning Strategies
URI Path (Default)
GET /api/v1/users
GET /api/v2/users
Header
GET /api/users
X-API-Version: 2
Query Parameter
GET /api/users?api_version=2
Accept Header
GET /api/users
Accept: application/vnd.api.v2+json
Subdomain Routing
For APIs served from a dedicated subdomain:
// config/apiroute.php 'strategies' => [ 'uri' => [ 'prefix' => '', // No /api prefix 'domain' => 'api.example.com', // Your API subdomain ], ],
GET https://api.example.com/v1/users
GET https://api.example.com/v2/users
Multi-Domain Routing
For resilience or redundancy scenarios where the same API is served on multiple domains:
// config/apiroute.php 'strategies' => [ 'uri' => [ 'prefix' => '', 'domain' => ['api.main.com', 'api.backup.com', 'api.proxy.com'], ], ],
All domains resolve to the same versioned routes:
GET https://api.main.com/v1/users
GET https://api.backup.com/v1/users
GET https://api.proxy.com/v1/users
Use environment variables for flexible configuration:
'domain' => array_filter(array_map('trim', explode(',', env('API_DOMAINS', '')))),
# .env API_DOMAINS=api.main.com,api.backup.com,api.proxy.com
Route names: when named routes (
->name(...), or the version'snameprefix) are registered on more than one domain, only the first domain in the list keeps the exact configured name โ soroute('api.users')stays backward compatible. Every additional domain automatically gets a unique, domain-derived suffix (e.g.api.api_backup_com.users) sophp artisan route:cachedoesn't fail with duplicate route name errors.Because of this, calling
route('api.users')always generates an absolute URL to the first configured domain, even from a request that came in on a secondary one. This was already true before route names were made unique (it previously pointed to whichever domain happened to be registered last) โ it's just predictable now.
Automatic Headers
On deprecated versions, responses include RFC-compliant headers:
HTTP/1.1 200 OK Deprecation: Sun, 01 Jun 2025 00:00:00 GMT Sunset: Mon, 01 Dec 2025 00:00:00 GMT Link: </api/v2/users>; rel="successor-version" X-API-Version: v1 X-API-Version-Status: deprecated
Deprecating a single endpoint
Beyond version-level deprecation, a single controller class, action, or route can be marked deprecated on its own, independently of the API version it belongs to.
Attribute
use Grazulex\ApiRoute\Attributes\Deprecated; #[Deprecated(since: '2026-01-01', successor: '/api/v2/legacy', docs: 'https://docs.example.com/legacy')] class LegacyController { #[Deprecated(sunset: '2026-06-01', successor: 'api.v2.things.index', reason: 'Use things v2')] public function index() { // ... } }
Method attributes override class attributes field by field; the macro overrides attributes.
Route macro
Route::get('/api/v2/closure', fn () => response()->json(['ok' => true])) ->deprecated(since: '2026-05-05', successor: '/api/v2/closure-v2');
Headers
| Header | When | Notes |
|---|---|---|
Deprecation |
since is set |
RFC 9745 header, emitted as an RFC 7231 HTTP-date (same format as the version headers) |
Sunset |
sunset is set |
RFC 8594 header, RFC 7231 HTTP-date |
Link |
successor and/or docs set |
rel="successor-version" and rel="deprecation" (RFC 9745) |
X-API-Endpoint-Status |
endpoint is deprecated or sunset | deprecated or sunset, gated by headers.include.endpoint_status |
successor is resolved in this order: a named route, a path starting with /, or an absolute URL. A named route is generated with the parameters of the current route (things/{id} can point to api.v2.things.show); when the URL cannot be generated, a warning is logged and no successor link is emitted.
Sunset policy
Once sunset is reached, the endpoint is handled by the api.endpoint-sunset middleware following the same policy as versions: apiroute.sunset.action (reject by default) and the status code from apiroute.sunset.status_code (410 by default).
This middleware is added automatically to routes registered inside ApiRoute::version() groups. Outside those groups, a deprecated route only gets the headers above; add api.endpoint-sunset to the route or group yourself to apply the 410 policy there.
PHP 8.4's native #[\Deprecated] attribute can be used alongside Grazulex\ApiRoute\Attributes\Deprecated on the same class or method; they serve different purposes (IDE/runtime deprecation notice vs. HTTP lifecycle) and do not conflict.
php artisan api:status lists deprecated endpoints (method, URI, version, dates, successor) in a dedicated table. With --json, the output keeps its historical shape (an object keyed by version); when deprecated endpoints exist, it becomes {"versions": {...}, "deprecated_endpoints": [...]}.
JSON:API
When a request sends Accept: application/vnd.api+json, version and endpoint errors are rendered as JSON:API error documents instead of the plain JSON body.
GET /api/v1/things Accept: application/vnd.api+json
{
"errors": [
{
"status": "410",
"code": "endpoint_sunset",
"title": "Endpoint sunset",
"detail": "Use things v2",
"links": {
"about": "https://docs.example.com/legacy",
"successor": "http://localhost/api/v2/things"
},
"meta": {
"sunset_at": "2020-01-01T00:00:00+00:00"
}
}
]
}
The error code is one of: version_not_found, invalid_version, version_sunset, endpoint_sunset.
Laravel 13 JSON:API resources
The InteractsWithApiVersion trait adds version metadata to a JsonApiResource document:
use Grazulex\ApiRoute\Http\Resources\InteractsWithApiVersion; use Illuminate\Http\Resources\JsonApi\JsonApiResource; class UserResource extends JsonApiResource { use InteractsWithApiVersion; // ... }
It merges a meta.api object (version, status, deprecation, sunset, successor) and, when a successor is resolvable, a top-level links.successor into the resource document. Endpoint-level deprecation takes precedence over version-level deprecation.
Artisan Commands
# View status of all API versions php artisan api:status # Create a new API version php artisan api:version v3 --copy-from=v2 # Mark a version as deprecated php artisan api:deprecate v1 --on=2025-06-01 --sunset=2025-12-01 # View usage statistics php artisan api:stats --period=30
Configuration
// config/apiroute.php return [ // API versions (v2.0+) 'versions' => [ 'v1' => [ 'routes' => base_path('routes/api/v1.php'), 'middleware' => [], 'status' => 'active', // 'active', 'beta', 'deprecated', 'sunset' 'deprecated_at' => null, 'sunset_at' => null, 'successor' => null, 'documentation' => null, 'rate_limit' => null, ], ], // Detection strategy: 'uri', 'header', 'query', 'accept' 'strategy' => 'uri', // Default version when none specified 'default_version' => 'latest', // Fallback behavior 'fallback' => [ 'enabled' => true, 'strategy' => 'previous', ], // Sunset behavior: 'reject', 'warn', 'allow' 'sunset' => [ 'action' => 'reject', 'status_code' => 410, ], // Response headers 'headers' => [ 'enabled' => true, 'include' => [ 'version' => true, 'deprecation' => true, 'sunset' => true, 'endpoint_status' => true, ], ], ];
Testing
composer test
Code Quality
# Run all quality checks composer full # Individual checks composer test:lint # Laravel Pint composer test:types # PHPStan composer test:unit # Pest
Changelog
Please see RELEASES for more information on what has changed recently.
Contributing
Please see CONTRIBUTING for details.
Security
Please review our security policy on how to report security vulnerabilities.
Credits
Thanks
- @maks-oleksyuk - Bug reports and testing
- @sameededitz - Feature request for subdomain and multi-domain routing
Support This Package
Laravel ApiRoute is free, open source and maintained on my own time. If it saves you hours, here is how you can give back:
- โญ Star the repository โ it helps other developers find it
- ๐ฆ Share it with your team and network
- ๐ Sponsor on GitHub, buy me a coffee or donate via PayPal โ every contribution funds maintenance, new features and Laravel upgrades
License
The MIT License (MIT). Please see License File for more information.