How to Design Idempotent and Versioned REST APIs
You deploy a new feature, and suddenly your API receives duplicate POST requests from clients retrying after a timeout. Your database now has two identical orders. Or you roll out a breaking change, and mobile apps crash because they expected the old response format. These are two of the most common and painful problems in API design: non-idempotent operations and breaking changes. This guide shows you how to solve both with practical patterns for idempotency and versioning in REST APIs.
Why Idempotency Matters in REST APIs
Idempotency means that making the same request multiple times has the same effect as making it once. HTTP methods have defined idempotency semantics: GET, HEAD, PUT, DELETE, and OPTIONS are idempotent, while POST and PATCH are not. But real-world APIs often need to accept POST for operations like creating resources or processing payments. Network failures, client timeouts, and retry logic can cause duplicates. Without idempotency, you risk double charges, duplicate records, or inconsistent state.
Techniques for Idempotent REST APIs
1. Use Idempotency Keys for POST Requests
The most common approach is to let clients send a unique key (e.g., a UUID) in an Idempotency-Key header. The server stores the key and the result of the operation. If the same key is received again, the server returns the stored result instead of re-executing the operation.
POST /payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
Implementation steps:
- Client generates a unique key (UUID v4) for each logical operation.
- Server checks if the key exists in a persistent store (e.g., Redis or database).
- If not, process the request and store the key with the response status and body.
- If yes, return the stored response without re-processing.
Set a reasonable expiration (e.g., 24 hours) to avoid unbounded storage growth.
2. Leverage Conditional Requests
For updates, use ETag and If-Match headers to prevent lost updates. The server returns an ETag representing the current state. The client sends it back with If-Match; if the resource changed, the server returns 412 Precondition Failed. This makes PUT requests safe against race conditions.
PUT /articles/123
If-Match: "abc123"
Content-Type: application/json
{"title": "Updated title"}
3. Design PUT for Natural Idempotency
Prefer PUT over POST when the client can determine the resource URL. For example, PUT /users/{userId} is naturally idempotent because repeated calls overwrite the same resource. Use POST only when the server assigns the ID.
4. Handle Duplicate Detection with Unique Constraints
Combine idempotency keys with database unique constraints on business keys (e.g., order number). If a duplicate slips through, the database rejects it, and you can return a 409 Conflict with a clear message.
Versioning Strategies for REST APIs
APIs evolve. New fields, changed behavior, or removed endpoints can break existing clients. Versioning lets you introduce changes without disrupting them. There are several common strategies:
| Strategy | Example | Pros | Cons |
|---|---|---|---|
| URI Path | /v1/users |
Explicit, easy to route, cache-friendly | URLs change, can clutter |
| Query Parameter | /users?version=1 |
Simple, optional | Easy to omit, not RESTful |
| Custom Header | Accept-Version: v1 |
Keeps URLs clean | Harder to test in browser |
| Media Type | Accept: application/vnd.api.v1+json |
Content negotiation, pure REST | Complex, less common |
For most teams, URI path versioning is the most pragmatic choice. It's visible, easy to document, and works well with API gateways and CDNs.
How to Version Without Breaking Clients
- Start with v1 in the path from day one. Adding versioning later is harder.
- Additive changes only within a version: new optional fields, new endpoints. Never remove or rename fields.
- Deprecate gracefully: announce end-of-life, provide migration guides, and monitor usage of old versions.
- Use sunset headers:
Sunset: Sat, 31 Dec 2025 23:59:59 GMTto inform clients. - Maintain at most two active versions to limit maintenance burden.
Putting It Together: A Practical Example
Consider a payment API. You want to create a payment idempotently and version the endpoint.
POST /v1/payments
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
Content-Type: application/json
{
"amount": 1000,
"currency": "USD"
}
Server logic:
- Check if
Idempotency-Keyexists in Redis. - If yes, return the cached response (status + body).
- If no, process payment, store key with response, return 201 Created.
For updates, use PUT with ETag:
PUT /v1/payments/123
If-Match: "xyz789"
Content-Type: application/json
{"amount": 1500}
If the ETag doesn't match, return 412. This prevents overwriting concurrent changes.
Common Pitfalls and How to Avoid Them
- Storing idempotency keys forever: set a TTL (e.g., 24h) to avoid storage bloat.
- Ignoring race conditions: use atomic operations (e.g., Redis SETNX) to check and set keys.
- Versioning too late: add /v1 from the start.
- Breaking changes in minor versions: treat any change that alters response structure as major.
- Not documenting idempotency: clearly state which endpoints support Idempotency-Key.
FAQ
What is an idempotent REST API?
An idempotent REST API ensures that making the same request multiple times produces the same result as making it once. This is crucial for handling retries safely, especially for non-idempotent methods like POST.
Which HTTP methods are idempotent?
GET, HEAD, PUT, DELETE, OPTIONS, and TRACE are idempotent. POST and PATCH are not idempotent by default, but you can make them idempotent using techniques like idempotency keys.
What is the best way to version a REST API?
URI path versioning (e.g., /v1/users) is the most common and practical approach. It's explicit, easy to route, and works well with caching and API gateways. Other options include query parameters, custom headers, and media types, but they have trade-offs.
Ready to test your API's JSON responses? Use our JSON Formatter to validate and pretty-print payloads quickly.