reindert-vetter/api-version-control
Laravel package for clean API versioning with version-specific controllers and routes. It detects the requested version from headers or URIs and dispatches requests accordingly, helping maintain backward-compatible endpoints without cluttering your application.
Install and configure the package in three steps:
composer require reindert-vetter/api-version-control
php artisan vendor:publish --tag=api-version-control
The published config controls how a version is detected and which versions are legal:
// config/api-version-control.php
return [
'version_parameter' => 'version', // query/route parameter name
'header' => 'accept', // header used for content negotiation
'strict' => false, // false = fall back, true = reject unknown
'versions' => ['v1', 'v2'],
];
Apply the middleware to the routes you want versioned:
// routes/api.php
Route::middleware('api.version')->group(function () {
Route::get('/users', 'UserController@index');
});
The first useful thing to try is versioning a single endpoint with the controller suffix convention: a method named indexV2 serves v2, while index (or indexV1) serves the fallback. Then call it with the configured header:
GET /api/users
Accept: application/vnd.myapp.v2+json
or ?version=v2, depending on which source you enabled. Where to look first: the package README (it is the source of truth for exact facade/helper names), the published config file, and the api.version middleware aliases registered by the service provider. Keep strict => false until you have real clients, then tighten it.
Middleware group per API surface. Put api.version on the whole routes/api.php group, not on individual routes, so every endpoint gets consistent resolution and fallback. If you have public and internal APIs, use separate groups with different config or middleware parameters.
Suffix convention for endpoint upgrades. index → indexV2 → indexV3 is the lowest-friction path: new versions add methods, old methods remain for older clients. Keep fallbacks deliberate; don't rename the base method once clients depend on it.
Branch inside the controller when a whole method isn't warranted. For small differences, resolve the active version once and branch:
if (\ReindertVetter\ApiVersionControl\Facades\ApiVersion::is('v2')) {
// v2 shape
}
Use this for response shape tweaks, not for core business logic — versioning should live at the edge (controllers/resources/requests), not deep in services.
Version the inputs too. Form requests are an easy place to branch validation rules (rules() can check ApiVersion::is('v2')). If a v2 request has a renamed field, handle the rename in the request or a small mapper, not in the model.
Version the output, not the schema. Keep one database schema and use API Resources/transformers per version. This avoids migrations being driven by API contract changes and keeps the version boundary explicit.
Deprecation signalling. When you ship v3, add Deprecation/Sunset response headers for older versions from a small middleware wrapping the version group. Clients see the warning without breaking.
Test the contract, not just the code. Write feature tests that set the version header and assert the payload shape per version. A matrix test (v1/v2/v3 × a few endpoints) catches accidental fallbacks and suffix typos.
Documentation per version. Generate or maintain OpenAPI specs keyed by version. If you only have one spec, at least annotate which fields are version-gated.
compare/2.3.2...2.3.3) before upgrading a production API — patch releases occasionally contain dependency or middleware tweaks that aren't in the notes.strict => false, a typo like indexV3 (when only indexV2 exists) falls back to an older method and may return a 200 with the wrong shape. Log the resolved version during development, and use strict => true in tests to catch invalid versions.Accept parsing is not just a substring. Accept headers can include media type parameters and multiple values. Send clean headers in tests, and verify how the package handles application/json with no version — it should hit your default, not a 500.versions or the header name, run php artisan config:clear (and route:clear if you altered route registration). Stale caches make version changes appear to do nothing.How can I help you explore Laravel packages today?