Laravel Apiroute
Complete API versioning lifecycle management for Laravel
#Laravel ApiRoute
[!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.