10 REST APIs: Versioning and Fleet Operations
Start with the story: A REST API is the map a device, app, or service uses to talk about real things: thermostats, readings, commands, firmware, and alerts. Good design makes those things obvious in the URL, uses HTTP verbs consistently, and returns errors that tell the next engineer what actually happened.
10.1 Start With the Decision
A thermostat API cannot break when old devices miss an update. Versioning, rate limits, and retry rules must protect the whole fleet.
10.2 Route Overview
This is part 2 of 2. Review REST APIs: Payload and Resource Design for the preceding evidence.
10.3 Learning Objectives
- Choose an API versioning strategy for deployed devices.
- Design rate-limit, error, security, and cache behavior.
10.4 Chapter Roadmap
- API Versioning Strategies
- Understanding API Versioning
- Checkpoint: Naming the Contract
- Error Response Format
- Rate Limiting and Throttling
- Understanding Rate Limiting
- Security Best Practices
- Checkpoint: Protecting the Fleet
- Case Study: Smart HVAC System API Design
- Case Study: Smart Building HVAC System
- Interactive: REST API Explorer Animation
- Worked Example: API Versioning Strategy for Deployed Thermostats
- Common Mistake: Ignoring API Rate Limiting on IoT Devices
- Common Pitfalls
- 1. Prioritizing Theory Over Measurement in REST API Design Patterns
- 2. Ignoring System-Level Trade-offs
- 3. Skipping Failure Mode Analysis
- Label the Diagram
- Order the Steps
- Match the Concepts
- Deep Dive: Methods, Idempotency, and Conditional Caching
- Checkpoint: Retry and Cache Semantics
- Summary
- Knowledge Check
- Quiz: REST API Design Patterns
- What’s Next?
10.5 API Versioning Strategies
IoT systems run for years - versioning prevents breaking deployed devices:
Core Concept: API versioning provides a contract between API providers and consumers that allows the API to evolve without breaking existing clients.
Why It Matters: IoT devices deployed in the field may run for 5-10 years without firmware updates. Without versioning, any API change (adding required fields, changing response formats, deprecating endpoints) will break thousands of devices simultaneously, causing service outages and costly emergency patches.
Key Takeaway: Always version from day one using URI path versioning (/v1/) for IoT APIs - it is the simplest approach that works across all protocols and is immediately visible in logs and debugging tools.
10.5.1 URI Versioning (Recommended for IoT)
# Version in path
coap://sensor.local/v1/temperature
coap://sensor.local/v2/temperature # New version with added metadata
# MQTT topic versioning
acme/v1/sensors/temp42/reading
acme/v2/sensors/temp42/reading
Pros: Simple, clear, works with any protocol Cons: Duplicate code if supporting multiple versions
10.5.2 Header Versioning
GET /temperature
Accept: application/vnd.iot.v1+json
Pros: Clean URLs Cons: Embedded devices may not support custom headers
10.5.3 Query Parameter
coap://sensor.local/temperature?version=1
Pros: Flexible, backward compatible Cons: Easy to forget, adds overhead
IoT-specific recommendation: Use URI versioning (/v1/, /v2/) because:
Put the version in the resource path when constrained clients need the contract to be obvious without custom-header parsing. A path such as /v1/temperature remains visible in device logs, proxy traces, and test fixtures, so a reviewer can tell which schema was used. Apply the same explicit namespace discipline to MQTT topics and CoAP resources where appropriate. The trade-off is duplicated routes during migration, which should be bounded by an owner, compatibility tests, and a retirement date rather than hidden behind an easy-to-forget query parameter.
Checkpoint: Naming the Contract
Before using the versioning calculator, verify that the API contract is readable without tribal knowledge:
- You now know why REST should expose resources such as
/devices/,/sensors/, and/readings/rather than action URLs such as/getTemperature. - You now know why MQTT topics need organization, location, device type, device id, and data type fields when a deployment grows to thousands of devices.
- You now know why URI versioning with
/v1/and/v2/is the default IoT choice: it is visible in logs, works across protocols, and avoids custom header parsing on constrained clients.
10.5.4 Interactive: API Versioning Migration Cost Calculator
Compare the cost of breaking existing devices (modifying v1) versus maintaining parallel API versions.
10.6 Error Response Format
Consistent error handling reduces debugging time:
Standard error structure (JSON):
{
"error": {
"code": "SENSOR_OFFLINE",
"message": "Device has not reported in 5 minutes",
"timestamp": "2025-01-15T10:30:00Z",
"device_id": "sensor-42",
"retry_after": 300
}
}
CoAP response codes:
2.01 Created - Resource created successfully
2.04 Changed - Resource updated
2.05 Content - Successful GET with payload
4.00 Bad Request - Invalid syntax
4.04 Not Found - Resource doesn't exist
5.00 Internal Server Error
MQTT error patterns:
# Publish errors to special topics
acme/errors/sensor-42 → {"code": "SENSOR_OFFLINE", ...}
# Or use QoS 0 for best-effort error reporting
10.7 Rate Limiting and Throttling
Protect infrastructure from device misbehavior:
Core Concept: Rate limiting restricts the number of API requests a client can make within a specified time window, protecting servers from overload and ensuring fair resource allocation across clients.
Why It Matters: In IoT systems, a single malfunctioning device or firmware bug can generate thousands of requests per second, overwhelming your cloud infrastructure and causing cascading failures that affect all devices. Rate limiting acts as a circuit breaker that isolates misbehaving devices while keeping the system operational for well-behaved clients.
Key Takeaway: Implement rate limits at multiple levels (per-device, per-tenant, per-endpoint) and always return meaningful error responses (HTTP 429 with Retry-After header) so clients can implement proper backoff strategies rather than hammering your servers.
Patterns:
# Per-device limits
Device temp42: 1 request/second max
Response: 429 Too Many Requests (HTTP)
4.29 Too Many Requests (CoAP)
# Per-tenant limits
Organization ACME: 10,000 messages/minute
MQTT: Disconnect with reason code (0x97 Quota Exceeded)
Implementation:
- Use token bucket algorithm (burst allowed, sustained rate limited)
- Return
Retry-Afterheader with backoff time - Log violations for debugging misbehaving devices
10.7.1 Interactive: Rate Limiting Cost Impact Calculator
Model the financial impact of missing rate limits when a firmware bug causes devices to over-poll.
10.8 Security Best Practices
Always authenticate and authorize:
# CoAP with DTLS
coaps://sensor.local/v1/temperature # Note: 's' for secure
# MQTT with TLS + auth
Username: device-42
Password: [device-specific token]
Client Certificate: [for mutual TLS]
# HTTP Bearer tokens
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
Design principles:
Protect the write path first: authenticate the caller, authorize the requested resource and method, and reject unauthenticated changes. Use TLS or DTLS across local as well as remote segments because link-layer protection ends at gateways and does not secure every later hop. Give devices distinct credentials, rotate and revoke them through a tested lifecycle, and rate-limit failed authentication without blocking legitimate recovery traffic. Preserve the principal, decision, and failure reason in the audit trail so the controls can be verified rather than merely configured.
Checkpoint: Protecting the Fleet
At this point the contract has names, formats, versions, errors, limits, and security boundaries:
- You now know why a fleet with
50,000deployed thermostats should add a/api/v2/response rather than surprising strict v1 parsers with a new field. - You now know why
429 Too Many Requests,Retry-After, and a token bucket belong in the design, not only in incident response after a polling bug. - You now know why TLS, DTLS, per-request authentication, and credential rotation are part of the REST pattern: stateless APIs cannot rely on server-side sessions to remember trust.
10.9 Case Study: Smart HVAC System API Design
Requirements:
Read these points as one connected sequence: start with 500 temperature sensors per building; then Real-time alerts for anomalies; then Historical data queries; and finish with Mobile app control.
- 500 temperature sensors per building
- Real-time alerts for anomalies
- Historical data queries
- Mobile app control
Solution - Hybrid approach:
# 1. MQTT for telemetry (sensors → cloud)
Topic: buildings/bldg1/floor3/zone-a/temp42/reading
Payload (CBOR): {t:23.5, h:45, ts:1642259400}
QoS: 0 (frequent updates, loss acceptable)
# 2. CoAP for control (app → actuators)
PUT coap://hvac.local/v1/zones/zone-a/setpoint
Payload: {"target":22.0}
Type: CON (confirmable - critical command)
# 3. HTTP REST for historical queries (app → cloud)
GET https://api.example.com/v1/sensors/temp42/history?start=2025-01-01
Response (JSON): [{"timestamp":"2025-01-01T00:00:00Z","value":23.5}...]
Why this works:
Read these points as one connected sequence: start with MQTT handles high-volume telemetry efficiently; then CoAP provides low-latency local control; then HTTP enables rich queries from mobile apps; and finish with Each protocol optimized for its use case.
- MQTT handles high-volume telemetry efficiently
- CoAP provides low-latency local control
- HTTP enables rich queries from mobile apps
- Each protocol optimized for its use case
10.9.1 Choosing the Right Data Serialization Format: A Decision Framework
Payload format choice has outsized impact on constrained IoT devices. Here is a quantitative comparison for a typical sensor reading {"temperature": 23.5, "humidity": 45, "timestamp": 1706140800}:
| Format | Encoded Size | Human Readable | Schema Required | Library Size (C) | Parse Speed |
|---|---|---|---|---|---|
| JSON | 62 bytes | Yes | No | 5-20 kB | Moderate |
| CBOR | 35 bytes | No (binary) | No (self-describing) | 2-5 kB | Fast |
| Protocol Buffers | 18 bytes | No (binary) | Yes (.proto file) | 30-100 kB | Very fast |
| MessagePack | 38 bytes | No (binary) | No (self-describing) | 3-8 kB | Fast |
| Custom binary | 12 bytes | No | Yes (manual) | 0 kB (hand-coded) | Fastest |
| If your project needs… | Choose… | Because… |
|---|---|---|
| Debugging ease, web dashboard integration | JSON | Universal tooling, browser-native, readable in logs |
| Compact payloads on constrained networks (LoRaWAN, 6LoWPAN) | CBOR | 40-50% smaller than JSON, self-describing, IETF standard (RFC 8949) |
| Maximum efficiency with versioned schemas | Protocol Buffers | 70%+ smaller than JSON, strong typing, backward-compatible evolution |
| Drop-in JSON replacement with size savings | MessagePack | JSON-compatible data model, ~40% smaller, minimal code changes |
| Extreme constraints (<1 kB payload budget) | Custom binary | Hand-pack fields at bit level, zero overhead, but no interoperability |
Quick Decision Flowchart:
Read these points as one connected sequence: start with Is the payload going over LoRaWAN (51-242 byte limit)? Yes —> CBOR or custom binary; then Do both ends share a compiled schema (.proto file)? Yes —> Protocol Buffers; then Is the API consumed by web browsers or curl? Yes —> JSON (use CBOR for device-to-gateway, JSON for gateway-to-cloud); then Do you need a drop-in replacement for JSON with smaller size? Yes —> MessagePack; and finish with Default: CBOR for device-to-gateway communication; JSON for cloud APIs and dashboards.
- Is the payload going over LoRaWAN (51-242 byte limit)? Yes —> CBOR or custom binary
- Do both ends share a compiled schema (.proto file)? Yes —> Protocol Buffers
- Is the API consumed by web browsers or curl? Yes —> JSON (use CBOR for device-to-gateway, JSON for gateway-to-cloud)
- Do you need a drop-in replacement for JSON with smaller size? Yes —> MessagePack
- Default: CBOR for device-to-gateway communication; JSON for cloud APIs and dashboards
10.9.2 Interactive: Fleet Bandwidth Savings Calculator
Estimate annual bandwidth and cost savings when migrating from JSON to a binary format across your IoT fleet.
Scenario: A smart thermostat manufacturer has 50,000 devices deployed running firmware v1.2 connecting to /api/v1/thermostats. They need to add a new field humidity to the response without breaking existing devices.
Option A: Add field to v1 (breaking change risk):
// Old v1 response (existing devices expect this)
{"temperature": 22.5, "mode": "heat"}
// New v1 response (adding humidity)
{"temperature": 22.5, "mode": "heat", "humidity": 45}
Risk: Devices with strict JSON parsing may reject extra fields
- Estimated 5% of devices have strict parsers
- 50,000 × 5% = 2,500 devices broken
Option B: Create /api/v2/ (recommended):
// v1 continues unchanged
GET /api/v1/thermostats/device-042
{"temperature": 22.5, "mode": "heat"}
// v2 includes new field
GET /api/v2/thermostats/device-042
{"temperature": 22.5, "mode": "heat", "humidity": 45}
Migration plan:
- Year 1: Both v1 and v2 active (0 downtime)
- Year 2: New firmware uses v2, old devices still on v1
- Year 3: Deprecation warning on v1
- Year 4: v1 sunset (98% devices upgraded by this point)
Cost comparison:
- Option A (modify v1): Emergency firmware push to 2,500 devices, support tickets, reputational damage = $125,000
- Option B (v2 endpoint): Dev cost for v2 endpoint + maintain both APIs for 2 years = $45,000
Decision: Create v2 endpoint. Savings: $80,000 + zero downtime.
The Error: Not implementing rate limits on device APIs, assuming “our devices are well-behaved.” A firmware bug causes 1,000 devices to poll every 100 ms instead of every 5 minutes.
Real Impact:
Normal traffic: 1,000 devices × 12 requests/hour = 12K req/hour
Bug traffic: 1,000 devices × 36,000 requests/hour = 36M req/hour (3,000× increase)
AWS API Gateway cost:
12K req/hour: $0.04/hour = $29/month (normal)
36M req/hour: $120/hour = $86,400/month (bug)
Damage: $86K bill + service outage for ALL devices (API throttled)
The Fix: Implement per-device rate limiting:
# Return 429 Too Many Requests after 20 requests/minute
@app.route('/api/v1/temperature')
@rate_limit(max_requests=20, window=60) # 20 per minute per device
def get_temperature():
return jsonify({"temp": 22.5})
Result: Bug triggers rate limit, affects only buggy devices. Bill capped at $150/month. Service continues for well-behaved devices.
Common Pitfalls
Relying on theoretical models without profiling actual behavior leads to designs that miss performance targets by 2-10×. Always measure the dominant bottleneck in your specific deployment environment — hardware variability, interference, and load patterns routinely differ from textbook assumptions.
Optimizing one parameter in isolation (latency, throughput, energy) without considering impact on others creates systems that excel on benchmarks but fail in production. Document the top three trade-offs before finalizing any design decision and verify with realistic workloads.
Most field failures come from edge cases that work in the lab: intermittent connectivity, partial node failure, clock drift, and buffer overflow under peak load. Explicitly design and test failure handling before deployment — retrofitting error recovery after deployment costs 5-10× more than building it in.
10.10 Deep Dive: Methods, Idempotency, and Conditional Caching
The patterns above cover URIs, versioning, and payloads. This layered walkthrough fills in the semantics that make a REST API survive a lossy IoT link: what each HTTP method promises, why idempotency decides whether a device can safely retry, and how ETag validators let a constrained device poll without re-downloading unchanged data.
REST models a system as resources named by URIs and acts on them with a small, agreed set of HTTP methods. A threshold should live under a noun such as /devices/42/config; the action belongs in PATCH or PUT, not in a custom endpoint like POST /setThreshold. Stable nouns make permissions, logs, test fixtures, and retry behavior easier to audit because each resource can list its allowed methods and expected outcomes once.
| Shape | Example | Design use |
|---|---|---|
| Collection | /devices | List devices with GET; create with POST when the server assigns ids |
| Item | /devices/42 | Read, replace, or delete one known device |
| Sub-resource | /devices/42/readings | Scope related data to its owner without inventing verbs |
| Anti-pattern | /setDeviceConfig | RPC-style URL hides method semantics from caches, proxies, and reviewers |
Two method properties matter most on lossy IoT links. A method is safe when it makes no intended state change, so clients may cache, prefetch, or repeat it. A method is idempotent when doing it many times leaves the same state as doing it once. When a device times out after sending a request, it often cannot tell whether the server received it; idempotency decides whether the retry is harmless.
| Method | Safe / idempotent | Typical IoT use |
|---|---|---|
GET | Safe, idempotent | Read a sensor value or config; success is usually 200 OK |
PUT | Not safe, idempotent | Replace a complete config resource; success is usually 200 OK or 204 No Content |
PATCH | Not safe, not guaranteed idempotent | Partially update config; make patches idempotent when devices may retry |
POST | Not safe, not idempotent | Create a resource or enqueue a command; use 201 Created or 202 Accepted |
DELETE | Not safe, idempotent | Remove a rule or device; repeated deletes should not recreate state |
A device that times out after PUT /devices/42/config can safely resend the same full representation, because two identical PUTs leave the same configuration. A blind retry of POST /commands can enqueue two commands. For retry-safe creation, either PUT to a client-chosen URI or send an idempotency key that the server deduplicates. Document that retry rule beside every method in the API contract.
Conditional requests solve the other expensive retry pattern: polling unchanged configuration. The server tags each representation with an ETag, such as ETag: "cfg-v7". The device stores that value and asks the server to send the body only if the representation changed:
GET /devices/42/config HTTP/1.1
Host: api.example
If-None-Match: "cfg-v7"
HTTP/1.1 304 Not Modified
ETag: "cfg-v7"
Cache-Control: max-age=30
If nothing changed, 304 Not Modified confirms the device is current without transferring the body. If the config changed, the server returns 200 OK, the new body, and a new ETag. The same validator protects writes: If-Match: "cfg-v7" on a PUT tells the server to apply the update only if the resource is still at version 7; otherwise it returns 412 Precondition Failed and avoids overwriting another client’s change.
Cache-Control decides how intermediaries may reuse the response. max-age=60 lets a gateway reuse a device list for 60 seconds, no-cache allows storage but requires revalidation before use, and no-store is the safer choice for tokens and secrets. A release test should cover all three validator cases: unchanged reads return 304 without a body, changed reads return 200 with a new ETag, and stale writes fail with 412.
For each REST endpoint, document three retry facts beside the request example: whether the method is idempotent, whether clients need an idempotency key, and which validator or cache header proves the response can be reused.
Checkpoint: Retry and Cache Semantics
The deep dive turns REST naming into operational behavior:
- You now know why repeated
PUTcan be safe for a full config replacement, while blindPOST /commandsretries can enqueue duplicate commands. - You now know how
ETag,If-None-Match,304 Not Modified,If-Match, and412 Precondition Failedprevent wasted downloads and stale writes. - You now know what to document for every endpoint before release: idempotency, idempotency keys, validators, cache headers, and the status code a device should expect after a retry.
10.11 Summary
This chapter covered practical REST API design patterns for IoT systems:
Key topics:
Read these points as one connected sequence: start with RESTful vs message-based patterns: Request-response for control, pub-sub for events; then Topic and URI naming: Consistent hierarchies for MQTT topics and CoAP URIs; then Payload format selection: JSON for debugging, CBOR/Protobuf for efficiency; then API versioning: URI versioning recommended for simplicity and compatibility; then Rate limiting and throttling: Token bucket algorithm, Retry-After headers; and finish with Security: TLS/DTLS, authentication on every request, credential rotation.
- RESTful vs message-based patterns: Request-response for control, pub-sub for events
- Topic and URI naming: Consistent hierarchies for MQTT topics and CoAP URIs
- Payload format selection: JSON for debugging, CBOR/Protobuf for efficiency
- API versioning: URI versioning recommended for simplicity and compatibility
- Rate limiting and throttling: Token bucket algorithm, Retry-After headers
- Security: TLS/DTLS, authentication on every request, credential rotation
10.12 Knowledge Check
10.13 What’s Next?
| Chapter | Focus | Why Read It |
|---|---|---|
| REST API Practice | Hands-on API design scenarios | Practice applying the patterns from this chapter to thermostat and fleet management case studies with step-by-step walkthroughs |
| MQTT Fundamentals | Publish-subscribe protocol deep-dive | Understand QoS levels, retained messages, Last Will and Testament, and broker architecture that make MQTT the dominant IoT telemetry protocol |
| CoAP Fundamentals and Architecture | Constrained Application Protocol | Learn how CoAP implements REST semantics over UDP with confirmable messages, observe mode, and resource discovery for low-power devices |
| Real-Time Protocol Workflows | Streaming and real-time IoT data | Explore WebSockets, Server-Sent Events, and RTSP for applications where sub-second latency matters — video surveillance, voice, and live monitoring |
| Protocol Selection Worked Examples | Multi-protocol comparison | Walk through decision frameworks for choosing between HTTP, MQTT, CoAP, and AMQP across six real IoT deployment scenarios |
| Application Protocols Overview | Protocol landscape and comparison | Review the full technical comparison of IoT application-layer protocols as a reference when making architecture decisions for new projects |
10.14 Continue Your Route
This final part closes the route from API Versioning Strategies through What’s Next?. Return to REST APIs: Payload and Resource Design or continue from the app-protocols module index.
