- Knowledge
- technology
- OOP
- Tips
- Programming
- Tips
- Tutorial
- SEO
- Ranking
- Knowledge
- Special Day
- Seo
- Bug
- Data science
- Seo
- artificial intelligence
- Machine Learning
- Robotics
- happyNewYear2021
- newYearEve
- 2021
- Automation
- Smart Home
- Career
- Best Practices
- Git
- Logging
- Web Fundamentals
- DNS
- HTTPS
- Performance
- AI Tools
- ChatGPT
- Claude
- Gemini
- Laravel
- Eloquent
- MySQL
- HTTPS
- TLS
- Web Security
- Certificates
- Developer Life
- Debugging
- Docker
- DevOps
- Transactions
- Queues
- LLMs
- AI
- AI Coding
- Developer Tools
- React Native
- Expo
- Kate PMS
- Mobile Apps
- Laravel
- Authentication
- Sanctum
- Cookies
- API Design
- Payments
- Idempotency
- DeepSeek
- Open Source AI
- LLMs
- AI News
- Git
- Version Control
- AI Coding
- Prompting
- PHP
- Checklist
- MCP
- AI Agents
- OpenAI
- Architecture
- Microservices
- Modular Monolith
- Estimation
- Developer Life
- Project Planning
- Humour
- OAuth
- OpenID Connect
- Authentication
- Embeddings
- Vector Search
- RAG
- pgvector
- OpenAI
- GPT-4.1
- Codex CLI
- Events
- Testing
- Clean Code
- Maintainability
- Code Review
- Webhooks
- API
- Security
- Claude Code
- Workflow
- AI
- LLM
- Prompt Injection
- Mobile
- React
- Networking
- TCP
- UDP
- HTTP/3
- CLAUDE.md
- AWS
- Cloud Security
- Backups
- PHPUnit
- Software Engineering
- Leadership
- Communication
- RAG
- Embeddings
- AI Engineering
- IT Infrastructure
- Networking
- Access Control
- CI/CD
- GitHub Actions
- Gemini CLI
- Claude Code
- JavaScript
- Async/Await
- Node.js
- Promises
- Security
- Cryptography
- Passwords
- MySQL
- Database
- Vibe Coding
- Software Quality
- DNS
- Code Reading
- Onboarding
- Productivity
- Background Jobs
- Developer Humour
- Estimates
- Dev Life
- JWT
- o3-mini
- DeepSeek R1
- Rate Limiting
- Kate PMS
- E-Signing
- Audit Trail
- REST
- GraphQL
- API Design
- Laravel 12
- Upgrade Guide
- Open Source
- Self-Hosting
- Task Scheduling
- Cron
- Secrets
- CORS
- PHP
- PHP-FPM
- OPcache
- GitHub Copilot
- Software Architecture
- Engineering
- TypeScript
- JavaScript
- Type Safety
- AI Security
- React Native
- Product Design
- AI Agents
- Kiro
- Queues
- Redis
- RabbitMQ
- AWS SQS
- Nginx
- Apache
- GPT-5
- gpt-oss
- Clean Code
- Architecture
- Naming
- Documentation
- Career
- ADR
- Teamwork
- Supply Chain
- Kate HRM
- HR Software
- Permissions
- System Design
- Pagination
- SSH
- Linux
- Big O
- Databases
- Laravel Boost
- MCP
- Developer Skills
- Validation
- Databases
- Indexes
- Code Quality
- Deployment
- Developer Humour
- Feature Flags
- Code Review
- Pull Requests
- Docker
- Cursor
- Authorization
- RBAC
- Gemini
- Long Context
- PHP 8.4
- Caching
- Dependency Injection
- Web Performance
- Browser
- CSS
- Database
- Migrations
- ChatGPT
- AI for Developers
- Monitoring
- On-Call
- REST
- Backend
- SQL
- NoSQL
- Database Design
- Coding Agents
- Claude 4
- API Resources
- REST API
- Load Balancing
- Scaling
- AWS
- AI Tools
- Claude
- Sora 2
- CTE
- 2FA
- TOTP
- Programming Languages
- Prompts
- Developer Workflow
- API Gateway
- APIs
- Passport
- API Auth
- Learning
- Burnout
- Developer Growth
- Web Development
- SEO
- Kate Mall
- ChatGPT Atlas
- Agent Skills
- Middleware
- Laravel 12
- Collections
- Context Window
- Monitoring
- Commit Messages
- Self Review
- Growth
- Regex
- Programming Basics
- Text Processing
- Database Design
- Normalization
- Linux
- Server Security
- Linux Foundation
- Open Standards
- Legacy Code
- Documentation
- AI Workflow
- File Uploads
- Test Data
- Hashing
- Performance
- Caching
- Enums
- Scope Creep
- Estimation
- Codex
- Gemini CLI
- Timezones
- Carbon
- Bugs
- PHP 8.5
- Gemini 3
- GPT-5.1
- Data Integrity
- Event Loop
- Async
- Opus 4.5
- AI Models
- React
- Forms
- Frontend
- Backups
- AI Images
- DALL-E
- Midjourney
- Race Conditions
- Concurrency
- Legacy Code
- Refactoring
- Senior Engineer
- Scope
- LLM
- CDN
- Web
- Sub-Agents
- Soft Deletes
- Audit Log
- Concurrency
- AI Learning
- NestJS
- AI Evals
- Policies
- SPF DKIM DMARC
- Unicode
- UTF-8
- Knowledge Graph
- Value Objects
- Technical Debt
- Feature Flags
- Laravel Pennant
- Deployment
- Copilot
- Composer
- Dependencies
- Artisan
- Automation
- AWS S3
- Object Storage
- Cloud
- Small Language Models
- Ollama
- Production
- Sessions
- HTTP
- Mentoring
- SQL
- Virtual Machines
- Web Development
- HTTP/2
- QUIC
- Web Performance
- AI Integration
- LLM API
- SOLID
- OOP
- Hosting
- Serverless
- Merge Conflicts
- Temperature
- AI Development
- Reverse Proxy
- Nginx
- Infrastructure
- Verification
- Passkeys
- WebAuthn
- Teams
- Communication
- Stakeholders
- Monorepo
- CI/CD
- Versioning
- JSON Schema
- Livewire
- Inertia
- Meetings
- Distributed Systems
- Privacy
- Full-Stack
- T-Shaped Skills
- Money
- Notifications
- Web Security
- HTTP Headers
- CSP
- Function Calling
- Load Testing
- k6
- Data Extraction
- Debugging
- WebSockets
- SSE
- Real-Time
- Laravel Reverb
- Infrastructure as Code
- Terraform
- Side Projects
- Laravel Pint
- OpenAPI
- Swagger
- UX
- Multimodal
- Jest
- Pair Programming
- APIs
- Rate Limiting
- Resilience
- Dev Humour
- Design Tokens
- JWT
- API Keys
- Sessions
- PHPStan
- Rector
- Incidents
- Reporting
- Dashboards
- Zero Trust
- IAM
- Search
- Laravel Scout
- Junior Developers
- Mentoring
- Images
- WebP
- AVIF
- Bug Reports
- Let's Encrypt
- Design Docs
- Software Design
- Observers
- Replication
- Accountability
- Data Structures
- Reliability
- LLM Memory
- Error Handling
- Payments
- Payment Gateway
- Webhooks
- PCI DSS
- Observability
- OpenTelemetry
- Personal Brand
- Writing
- Conventions
- Dates
- Scheduling
- Disaster Recovery
- Compression
- Brotli
- Deadlines
- Developer Habits
- State Machines
- Tech Roles
- UUID
- ULID
- Horizon
- Planning
- Engineering Culture
- Ownership
- Soft Skills
- Socialite
- Cost Control
- Collations
- Unicode
- Octane
- PostgreSQL
API Versioning Strategies That Won't Trap You Later
About Post
Releasing v2 of an API takes an afternoon. Switching off v1 can take years.
That's the part nobody plans for. Somewhere out there is a phone that hasn't updated your app since the day it was installed, a partner integration written by a contractor who has moved on, and a cron job on a server nobody remembers. They all call v1, and they will keep calling it long after you've stopped thinking about it.
So the real question isn't "how do I put a version number in my API?" It's "how do I change my API without breaking the clients I can't control?" The version number is a small part of the answer.
First, know what actually breaks a client
Most changes don't need a new version at all. The trick is being honest about which ones do.
Usually safe (additive):
- Adding a new endpoint.
- Adding a new optional field to a response.
- Adding a new optional request parameter with a sensible default.
Breaking:
- Removing or renaming a field, even one you're sure "nobody uses".
- Changing a type:
"amount": "1500.00"becoming"amount": 1500. - Making an optional parameter required, or tightening validation.
- Changing what a value means, such as a date that used to be local time and is now UTC.
- Adding a new value to an enum that clients switch on, like a new
status.
That last one surprises people. Adding "status": "on_hold" looks additive from the server's side. But a mobile app with a switch over the known statuses may crash, or show a blank screen, when it meets a value it has never seen. Whether it's breaking depends on how clients were written, which is exactly why you should tell client developers from day one: ignore unknown fields and handle unknown enum values gracefully.
The three common strategies
| Strategy | Looks like | Good | Not so good |
|---|---|---|---|
| URL path | /api/v1/invoices | Obvious, easy to test in a browser or curl, easy to route and log | "Purists" say a resource shouldn't change its URL; whole-API versions can feel heavy |
| Header | Accept: application/vnd.acme.v2+json or a custom version header | Clean URLs; can version per resource | Invisible in logs and links, easy to forget, harder to debug and cache correctly |
| Dated versions | A header like Api-Version: 2026-04-01, pinned per client | Small, frequent changes without big-bang releases | Needs real machinery: a transformation layer per version change |
There's also the query string (?version=2). It works, but it mixes the version with filters and paging, and it's easy to drop when someone builds a URL by hand.
Dated versions are the approach Stripe is well known for: each account is pinned to the API version it started with, and the server translates responses for older versions. It's elegant, and it's a lot of engineering. If you're a small team, you probably don't need it.
My pick for most teams: the URL, used rarely
For most product APIs, I'd put a major version in the path and then try very hard never to need v2.
The URL wins on the boring things that matter at 2 a.m.: you can see the version in every access log line, every bug report and every screenshot. A support person can tell which version a client calls without reading headers. That visibility is worth more than URL purity.
The "used rarely" part is the real strategy. A new major version should be an event, a few times in the life of a product, not something you do every time a response changes. Additive changes go into the current version. Breaking changes get collected, discussed, and released together.
In Laravel 12 the routing side is simple:
// routes/api.php (already prefixed with /api)
use App\Http\Controllers\Api\V1;
use App\Http\Controllers\Api\V2;
Route::prefix('v1')->middleware('auth:sanctum')->group(function () {
Route::get('invoices', [V1\InvoiceController::class, 'index']);
});
Route::prefix('v2')->middleware('auth:sanctum')->group(function () {
Route::get('invoices', [V2\InvoiceController::class, 'index']);
});
A tip that saves a lot of duplication: version the shape, not the business logic. Both controllers can call the same service; only the API resource (the class that turns a model into JSON) differs. When v1 is finally switched off, you delete a folder of thin classes, not half your application.
Deprecation is a process, not an announcement
An email saying "v1 will be switched off in six months" reaches the people who read emails. The client code doesn't read emails. So tell the code too.
There are standard HTTP headers for this. Sunset (RFC 8594) says when an endpoint will stop working, and the newer Deprecation header marks a response as coming from a deprecated endpoint. A small middleware on the v1 group is enough:
public function handle(Request $request, Closure $next): Response
{
$response = $next($request);
$response->headers->set('Sunset', 'Wed, 30 Sep 2026 23:59:59 GMT');
$response->headers->set('Link', '<https://example.com/docs/api/v2>; rel="successor-version"');
Log::channel('api-deprecation')->info('v1 call', [
'client' => $request->user()?->id,
'path' => $request->path(),
'agent' => $request->userAgent(),
]);
return $response;
}
The logging line matters more than the headers. Before you switch anything off, you want a list of who still calls v1 and how often. Then deprecation becomes a short list of real clients to contact, not a guess.
Rule of thumb: never switch off an API version on a date. Switch it off when your logs say nobody important is still using it, and give the stragglers a warning they can't miss.
Mobile apps change the maths
With a web frontend, you deploy the new JavaScript and every user gets it on the next page load. With a mobile app, you ship a build, the store reviews it, and users update when they feel like it. Some never do.
That means the API has to support several app versions at once, for a long time. A few habits make this manageable:
- Send the app version with every request, for example in a custom header. Now your logs can tell you exactly which builds are still active.
- Have a minimum supported version. An endpoint the app calls at startup can return the minimum version, and the app shows a friendly "please update" screen below it. Build this into version 1.0 of the app. You can't add it later to the builds that are already out there.
- Prefer additive changes even more. A new optional field costs you nothing. A renamed field costs you a forced update for every user.
- Test old builds against the new API before deploying. Keep an older build installed on a test device for exactly this.
This is the same thinking I apply whenever a web portal and a mobile app share one backend: the web side moves at your pace, the app side moves at the user's pace, and the API has to respect both.
The short version
- Most changes should be additive. Those don't need a new version.
- Know the sneaky breaking changes: types, meanings, tightened validation, new enum values.
- Put a major version in the URL for visibility, and release new majors rarely.
- Version the response shape, share the business logic.
- Deprecate with headers and logs, not just emails.
- For mobile apps, send the app version and ship a "please update" switch from day one.
What's the oldest API version you still have running in production, and what's keeping it alive?

Be first to comment it...