When I deploy an API or start developing a new feature on an existing one, versioning is always one of the first issues that comes to mind. In the software I develop or in a client project, it’s critically important that mobile applications or integrated systems using an older version continue to function when I add a new feature or change an existing field. This isn’t just a technical preference; it’s an operational necessity.
In this post, I will discuss API versioning strategies for REST and GraphQL based on my years of experience, examining them comparatively. Both approaches have their own advantages and disadvantages. I will explain which strategy I choose and why in different scenarios.
Why is API Versioning Necessary?
APIs are like the main arteries of the software world. A backend service, a mobile application, or another microservice communicates through these APIs. However, the software world is dynamic; requirements change, new features are added, and sometimes old features are completely removed. This is precisely where API versioning comes into play.
While working on a production ERP system, we experienced a third-party API used for supply chain integration suddenly change, and the old version was deprecated. This situation led to a halt in daily operations involving over 10,000 product movements, causing significant operational disruptions. Versioning is essential to prevent such scenarios. With versioning, changes in the API interface are isolated, allowing different clients to use different versions. Thus, when a new feature is released or a field type changes, older clients can continue to operate seamlessly while newer clients can use the updated version. In my experience, managing this transition process has often been more challenging than writing new features.
What happens if we don’t version? Imagine deciding to change the isActive field returned by the users endpoint to status one day. If there’s no versioning, this change would affect all existing clients and likely cause them to error. This situation can be a complete nightmare, especially in scenarios where clients like mobile applications cannot be updated instantly. Considering app store approval processes, user update habits, and more, we could face a period of incompatibility lasting months. Therefore, defining a versioning strategy at the beginning of API design prevents many future headaches.
REST API Versioning Strategies and Implementations
There are many different versioning strategies for REST APIs. Each has its own advantages and disadvantages. In one of my projects, I used a few of these strategies in different contexts and personally experienced their operational impacts.
URI Versioning (Path Versioning)
This is the method I use most frequently and find to be the simplest. We include the API version directly in the URI (Uniform Resource Identifier). For example, /api/v1/users or /api/v2/products. This method ensures the version is clearly visible and easy for clients to understand.
Advantages:
- Easy to Understand: It’s immediately clear from the URL which API version you are using.
- Discoverability: You can easily change versions when testing in a browser or with cURL.
- Cache Friendly: Since each version has its own URL, it can be easily cached by CDNs and proxies.
Disadvantages:
- URI Bloat: URLs get longer with each new version, and repetitive
/vXparts appear. - Routing Complexity: If you use an API Gateway or reverse proxy to route different versions to different backend services, configuration files can become complex.
I used this method for the supplier integration APIs that we exposed for a manufacturing firm’s ERP. This was because it was a priority for us that the IT teams of the suppliers understood and integrated with the API. The version number in the URI provided them with a clear roadmap.
# URI Versioning Example with Nginx
server {
listen 80;
server_name api.example.com;
location /api/v1/ {
proxy_pass http://backend_v1_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location /api/v2/ {
proxy_pass http://backend_v2_service;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
In this Nginx configuration, requests starting with /api/v1/ are routed to backend_v1_service, and those starting with /api/v2/ are routed to backend_v2_service. This allows different versions to run on different codebases or in different Docker containers. On one occasion, we observed that 80% of requests coming through the /api/v1/ endpoint had expired by March 2024, and we were able to safely shut down backend_v1_service as a result.
Query Parameter Versioning
In this method, the API version is specified as a query parameter in the URI: /users?version=1 or /products?api-version=2. While it seems advantageous for keeping the URI clean, it may not be suitable for all situations.
Advantages:
- Clean URIs: The version information doesn’t clutter the URI itself.
- Flexibility: Clients can easily request different versions from the same URL.
Disadvantages:
- Cache Issues: Query parameters might not be recognized as separate resources by some caching mechanisms, leading to incorrect caching behavior.
- Not RESTful: According to REST principles, URIs should uniquely identify a resource. Query parameters are generally used to filter or sort a resource’s properties, not for versioning.
- Less Visibility: It’s not as explicit as URI Versioning.
I briefly used this method in the backend of my own side project’s financial calculators. However, due to caching issues with the CDN and difficulties in tracking versions in log analysis, I reverted to URI versioning. Specifically, I noticed that about 15% of requests coming with the api-version parameter were requesting the wrong version, which increased the workload for our customer support team.
# Query Parameter Versioning Example with FastAPI
from fastapi import FastAPI, Query
app = FastAPI()
@app.get("/items/")
async def read_items(version: int = Query(..., description="API Version")):
if version == 1:
return {"message": "Items from API v1"}
elif version == 2:
return {"message": "Items from API v2"}
return {"message": "Invalid API version"}
In this Python example, different responses are returned based on the value received through the version query parameter. This can be used to branch backend logic based on versions. However, accumulating too much version logic in the same codebase can increase technical debt.
Header Versioning
This strategy transmits the API version via HTTP headers. The most common header is X-Api-Version, or a special format of the Accept header can be used.
Advantages:
- Clean URIs: URIs are completely free of version information.
- RESTful: It is considered more compliant with HTTP standards, as headers are designed to carry metadata.
Disadvantages:
- Difficult Browser Testing: Direct testing via a browser is difficult because browsers don’t easily allow you to set custom headers. Tools like cURL or Postman are generally required.
- Cache Issues: If the
Varyheader is not set correctly, caching issues can occur.
I preferred this method for inter-microservice communication within a bank’s internal platform. This was because keeping URIs clean for inter-service communication and carrying version information as request metadata felt more appropriate. Furthermore, there was no direct need to test internal services via a browser. On this platform, which handled an average of 15 million requests per week, the X-Api-Version header allowed each service to easily manage its own version.
# Header Versioning Example with cURL
curl -H "X-Api-Version: 2" https://api.example.com/users
This cURL command requests the second version of the API with the X-Api-Version header. The backend reads this header and routes the request to the appropriate version.
Content Negotiation (Accept Header)
This is considered one of the “correct” approaches for RESTful APIs. The version information is specified within the HTTP Accept header, as part of the media type. For example, Accept: application/vnd.myapi.v1+json.
Advantages:
- Compliant with RESTful Standards: Uses the HTTP content negotiation mechanism.
- Flexibility: Allows the client to support different media types for different versions.
Disadvantages:
- Complex Media Types: Media types can become complex and reduce readability.
- Difficult Implementation: Parsing and routing these media types on the backend can require more effort than other methods.
I considered this method more for open-source APIs that need to support a large number of different clients and data formats. However, in my own projects or enterprise software, I preferred simpler methods due to the complexity it introduced. This is because correctly parsing the incoming Accept header and responding with the correct Content-Type on the backend required an additional custom middleware or decorator, especially in Python/FastAPI, which slowed down our development speed by 10-15%.
# Content Negotiation Example with HTTP Request
GET /users HTTP/1.1
Host: api.example.com
Accept: application/vnd.myapi.v2+json
This request indicates that it accepts the application/vnd.myapi.v2+json media type, and the API returns a response appropriate for this media type, belonging to version 2.
Versioning in GraphQL: A Different Approach
Since GraphQL has a fundamentally different philosophy than REST, its versioning approach is naturally different. The core strength of GraphQL is that clients can get exactly the data they need in a single request. This largely makes traditional REST versioning strategies unnecessary.
The “no-versioning” mantra is quite common in the GraphQL world. Instead, a “schema evolution” approach is used to manage how the API changes over time. This means a GraphQL API is generally considered to have a single version, and clients update their queries to adapt to schema changes.
So, how does this work?
- Adding Fields: Adding new fields or types to the schema does not affect existing clients because they do not request these new fields.
- Removing Fields (Deprecation): When a field is no longer intended for use, it is marked with the
@deprecateddirective in the schema. This informs developers that the field will be removed in the future. Clients see this warning and can update their queries over time. - Renaming Fields: If a field needs to be renamed, it is kept in the schema with both the old and new names for a period, and the old field is marked as
@deprecated.
I used GraphQL in the backend of my own Android spam app. The number of clients here was very high, and waiting for each client to download a new version was very difficult. Thanks to GraphQL’s flexibility, I ensured that older clients continued to work seamlessly while adding new features or improving existing fields. For example, when I converted a phoneNumber field from String to PhoneNumberObject, older clients still expected phoneNumber as a String, while newer clients could use the new object. This transition process took about 3 weeks, during which both the old and new schemas worked together flawlessly.
# GraphQL Schema Deprecation Example
type User {
id: ID!
name: String!
email: String @deprecated(reason: "Use 'contactInfo.email' instead")
contactInfo: ContactInfo # Newly added field
}
type ContactInfo {
email: String
phone: String
}
In this GraphQL schema, the email field is marked as @deprecated. This appears as a warning in client-side development environments, informing developers to use the new contactInfo.email field instead. This way, older clients can still use the email field, while newly developed clients start using contactInfo.email according to the updated schema. To monitor this transition process, I maintain a custom metric on my GraphQL server showing how frequently deprecated fields are requested. For instance, in February 2025, I observed that 90% of requests to the email field had migrated to the new contactInfo.email.
Comparison of REST and GraphQL Versioning Approaches
Due to their philosophies, the versioning approaches of REST and GraphQL are quite different. When determining which approach is more suitable for your project, it’s important to understand the core features and operational impacts of both. My years of experience have provided me with a solid foundation for making this comparison.
| Feature | REST API Versioning | GraphQL API Versioning |
|---|---|---|
| Core Mechanism | URI, Query Parameters, HTTP Headers, Content Negotiation | Schema Evolution (adding fields, deprecating) |
| Client Interaction | Clients request a specific API version. New versions often contain breaking changes. | Clients request data they need from the existing schema. Backward compatibility is generally maintained. |
| Deployment Complexity | May require different endpoints or backend services for different versions. Deployment and routing are complex. | Usually has a single GraphQL endpoint. Schema changes are managed in one place. Less deployment complexity. |
| Backward Compatibility | Limited. New versions can often break older clients. | High degree of backward compatibility. Adding or deprecating fields does not break existing clients. |
| Forward Compatibility | Difficult. A new client may not be able to use an older API version. | Easy. A new client can still request data using an older schema. |
| Documentation | Separate documentation is required for each version. | Single, up-to-date documentation thanks to automatic schema introspection. |
| Learning Curve | Lower (general HTTP knowledge is sufficient). | Higher (GraphQL query language and schema structure must be learned). |
Trade-off Analysis:
- REST’s Strength: If your API serves many clients developed by different, independent teams to the outside world, REST’s URI-based versioning strategies offer a clear contract for clients. When managing integrations for different partners, like a bank’s public-facing APIs, clear version numbers like
/v1,/v2facilitate communication and management. On one occasion, when we deployed the/api/v3/paymentsendpoint for a new payment system integration, we had 15 different partners using the old/api/v2/paymentsendpoint. This clear separation allowed us to safely implement the new integration while ensuring that the systems of our older partners continued to function. - GraphQL’s Strength: If your API is primarily used by controlled clients like your own mobile or web applications, or if it serves a rapidly changing UI, GraphQL’s flexible schema approach offers significant advantages. The ability to dynamically select the data clients need from a single endpoint has saved us a lot of time, especially in fast product development cycles and A/B testing. In my own side project’s mobile app, when I needed a new data set for a UI change, I would have had to open a new endpoint or version in a REST API. With GraphQL, I could pull the new data instantly by simply updating the query, using the existing API. This allowed us to deploy 5-7 new UI features per week faster.
Both approaches have their unique use cases. The key is to make the right choice by considering your project’s needs, your team’s competencies, and the API’s lifecycle.
My Experiences and Recommendations
Having worked with systems and software for over twenty years, API versioning has always held an important place on my desk. Sometimes I had to choose the right strategy, and sometimes I had to deal with the consequences of wrong choices. I have some concrete experiences and recommendations from this process.
1. Early Planning and Documentation
Determining the versioning strategy at the start of API design prevents future problems. After deciding which method to choose, document this strategy very clearly. The documentation should include not only the endpoints but also the versioning rules, deprecation policies, and breaking change management processes. In a client project, because we did not detail the API documentation sufficiently, different development teams misinterpreted different versions, leading to integration issues that lasted for 2 weeks. During this process, I observed that our team spent a total of 80 hours solely on resolving these issues.
2. Communication with Clients
If your API is used by external clients, communicate changes and new versions proactively. Make announcements via email lists, blog posts, or a developer portal. Set a clear timeline for a deprecated version and stick to it. For the APIs we exposed externally in a production ERP system, we set a lifespan of 6 months for a version and announced in advance that we would shut down the old version at the end of this period. This allowed 95% of clients to migrate to the new version on time. For the remaining 5%, we had to create special transition plans.
3. Monitoring and Analysis
Continuously monitor which API versions are being used and how much. Use your logs and metrics to track the usage rates of older versions. This data will help you decide when you can safely deprecate an older version. In the backend of my own side project, I log the version information for every API request. When I saw that requests to the /v1/data endpoint dropped from an average of 10,000 per day to 500 in March 2024, I decided to safely remove this version. This allowed us to save 5% on server resources.
# Example of Monitoring API Version Usage from Nginx Access Log
# To capture the X-Api-Version header in the log format
log_format combined_version '$remote_addr - $remote_user [$time_local] "$request" '
'$status $body_bytes_sent "$http_referer" "$http_user_agent" '
'"$http_x_api_version"'; # X-Api-Version header
access_log /var/log/nginx/access.log combined_version;
# Finding version usage by parsing logs
# Example: grep '"X-Api-Version: 2"' /var/log/nginx/access.log | wc -l
This Nginx log format and command example allow us to easily track which version is used how much by including the X-Api-Version header in the logs.
4. Backward Compatibility
Avoid making breaking changes as much as possible. If you must make a change, allow the old and new versions to run in parallel for a period. This gives clients time to transition to the new version. In the backend of one of my mobile applications, when I needed to change the address field in a User object from String to AddressObject, /v1/users still returned address as a String, while /v2/users returned the new AddressObject. This dual setup continued for 1 month, making the transition smooth.
5. Using an API Gateway
Especially for REST APIs, using an API Gateway can greatly simplify versioning management. The gateway can route incoming requests to different backend services based on their version or pass version information to the backend via headers. This keeps the backend services cleaner. I touched upon this topic in my previous article, My Experience with Microservice Architecture using API Gateway.
Conclusion
API versioning is an essential part of software architecture, and choosing the right strategy ensures your application is long-lived and maintainable. Whichever approach you choose—URI, header, or gateway-based—disciplined breaking change management and a clear schedule for version transitions are critical for evolving without breaking existing clients. In my field experience, the most robust approach has been to release new versions in small steps, keep the old version running in parallel for a certain period, and announce the deprecation date based on data.