API Versioning for Web Application
API versioning is a way to introduce breaking changes without breaking existing clients. Without versioning, any schema change, field removal, or endpoint renaming will break mobile apps, partner integrations, and scripts you don't control.
Versioning Strategies
URL versioning — the most explicit approach:
GET /api/v1/articles
GET /api/v2/articles
Pros: obvious from the URL, easy to cache on CDN, simple for developers. Cons: URL becomes "cluttered" with version, resource /articles is duplicated.
Header versioning:
GET /api/articles
Accept: application/vnd.myapp.v2+json
Cleaner from a REST perspective, but harder to test (curl requires explicit header), caches poorly without Vary: Accept.
Query parameter:
GET /api/articles?version=2
Only for edge cases — mixes version with business request parameters.
Recommendation: URL versioning for most projects. Header versioning if API is already in production and URLs cannot be changed.
Implementation in Laravel
// routes/api.php
Route::prefix('v1')->group(base_path('routes/api_v1.php'));
Route::prefix('v2')->group(base_path('routes/api_v2.php'));
// routes/api_v2.php
Route::apiResource('articles', App\Http\Controllers\V2\ArticleController::class);
V2 controllers inherit from V1, overriding only changed methods:
namespace App\Http\Controllers\V2;
use App\Http\Controllers\V1\ArticleController as V1Controller;
class ArticleController extends V1Controller
{
public function index(Request $request)
{
// V2: added excerpt field, removed body from list
return ArticleV2Resource::collection(
Article::paginate($request->per_page ?? 20)
);
}
}
Implementation in NestJS
// main.ts
app.setGlobalPrefix('api');
// modules/v1/v1.module.ts and v2/v2.module.ts
@Controller('v1/articles')
export class ArticleV1Controller { ... }
@Controller('v2/articles')
export class ArticleV2Controller { ... }
Or via NestJS versioning API:
app.enableVersioning({ type: VersioningType.URI });
@Controller({ path: 'articles', version: '2' })
export class ArticleV2Controller {
@Get()
findAll() { ... }
}
Version Lifecycle
Typical process:
- New version is announced in
Changelogwith list of breaking changes - Old version gets
deprecatedstatus —Sunsetheader in responses - After 6–12 months, old version is turned off
// Middleware adds Deprecation header to V1 responses
class AddDeprecationHeader
{
public function handle($request, Closure $next)
{
$response = $next($request);
if (str_starts_with($request->path(), 'api/v1/')) {
$response->headers->set('Deprecation', 'true');
$response->headers->set('Sunset', 'Sat, 01 Jan 2026 00:00:00 GMT');
$response->headers->set('Link', '<https://api.example.com/v2/>; rel="successor-version"');
}
return $response;
}
}
What Constitutes a Breaking Change
Not all changes require a new version. Safe changes (backward-compatible):
- Adding a new field to response
- Adding a new optional request parameter
- Adding a new endpoint
- Adding a new enum value (if client ignores unknown values)
Breaking changes requiring a new version:
- Removing a field from response
- Renaming a field
- Changing field type (
string→integer) - Changing format (
2024-01-15→1705276800) - Removing an endpoint
- Changing method semantics (e.g., PATCH behaves like PUT)
API Changelog
Versioning without documenting changes is useless. CHANGELOG.md format for API:
## v2.0.0 (2025-03-01)
### Breaking Changes
- `GET /articles` — removed `body` field, added `excerpt` (first 200 characters)
- `POST /articles` — `tags` field now array of IDs, not strings
### New Features
- `GET /articles/{id}/related` — related articles
- Cursor-based pagination: `after` parameter instead of `page`
## v1.x — Deprecated
Supported until 2026-01-01. Use v2.
Versioning OpenAPI Specifications
Separate file per version:
docs/
openapi.v1.yaml
openapi.v2.yaml
Or via $ref between files — reuse common schemas (ErrorResponse, Pagination) without duplication.
Timeline
Setting up URL versioning with route separation, controller inheritance, Deprecation headers: 2–3 days. With automatic changelog, Sunset monitoring (alert on deadline exceeded), and separate OpenAPI files: 1 week.







