1217  CoAP API Design Best Practices

1217.1 Learning Objectives

By the end of this chapter, you will be able to:

  • Design RESTful CoAP APIs: Structure resources as nouns with proper URI conventions
  • Select Content Formats: Choose between JSON, CBOR, and text/plain for payloads
  • Implement Error Handling: Use proper CoAP response codes with helpful error messages
  • Apply Security Practices: Configure DTLS, rate limiting, and access control

1217.2 Prerequisites

Before diving into this chapter, you should be familiar with:

1217.3 RESTful Resource Design

TipMinimum Viable Understanding: CoAP REST Design

Core Concept: CoAP follows REST principles - design resources as nouns, use HTTP-like methods as verbs. The URI identifies WHAT youโ€™re accessing, the method specifies HOW.

Why It Matters: Consistent RESTful design makes APIs intuitive for developers, enables caching, supports proxying, and allows standard tooling to work seamlessly.

Key Takeaway: Good resource design: coap://sensor.local/v1/temperature (noun). Bad design: coap://sensor.local/getTemperature (verb in URL).

1217.3.1 Good vs. Bad Resource Design

Good resource design (nouns):

coap://sensor.local/v1/temperature      # Resource (noun)
coap://sensor.local/v1/humidity         # Resource (noun)
coap://actuator.local/v1/led/state      # Resource (noun)
coap://gateway.local/v1/config/network  # Resource (noun)

Poor resource design (verbs - avoid):

coap://sensor.local/getTemperature      # Verb in URL - wrong!
coap://actuator.local/turnOnLED         # Verb in URL - wrong!

REST operations on resources:

GET    coap://sensor.local/v1/temperature     # Read current value
PUT    coap://actuator.local/v1/led/state     # Update LED state
POST   coap://gateway.local/v1/logs           # Create new log entry
DELETE coap://gateway.local/v1/logs/2025-01   # Delete old logs

1217.3.2 URI Naming Conventions

CoAP URIs should be short (constrained bandwidth) but descriptive:

Recommended structure:

coap://{host}/{version}/{resource_type}/{device_id}/{subresource}

Examples:
coap://sensors.local/v1/devices/temp42/reading
coap://sensors.local/v1/devices/temp42/metadata
coap://sensors.local/v1/devices/temp42/config

Best practices:

Practice Example Rationale
Version your API /v1/ Allows future changes without breaking clients
Use plural nouns /devices/temp42 Consistent collection semantics
Keep it short Every character counts Constrained networks
Lowercase with hyphens /motion-sensors URL standard
Avoid deep nesting Max 3-4 levels /devices/sensors/temp/reading is too deep

1217.3.3 Payload Format Selection

Choose content format based on your constraints:

Content-Format Code Size Use Case
text/plain 0 Minimal Simple sensor values: โ€œ23.5โ€
application/json 50 Large Development, debugging
application/cbor 60 Small Production constrained devices
application/octet-stream 42 Varies Binary sensor data

Example - Temperature reading in different formats:

# Text/plain (4 bytes) - best for simple values
"23.5"

# JSON (42 bytes) - good for debugging
{"device":"temp42","value":23.5,"unit":"C"}

# CBOR (20 bytes) - production choice
A3 66 64 65 76 69 63 65 66 74 65 6D 70 34 32...

Recommendation:

  • Battery sensors: Use text/plain or CBOR (minimize bytes)
  • Development: Use JSON (easy debugging)
  • Production: Use CBOR (efficient, supports rich structures)

1217.3.4 Versioning Strategy

IoT devices often run for years - plan for API evolution:

URI versioning (recommended for CoAP):

coap://sensor.local/v1/temperature    # Original API
coap://sensor.local/v2/temperature    # New version with metadata

Why URI versioning for IoT: - Simple for embedded clients - No custom header parsing needed - Clear in logs and debugging - Works with all CoAP libraries

Version migration example:

# v1: Simple temperature reading
GET coap://sensor.local/v1/temperature
Response: "23.5"

# v2: Rich metadata added
GET coap://sensor.local/v2/temperature
Response (CBOR): {"value":23.5,"unit":"C","timestamp":1642259400}

# Legacy devices continue using v1
# New devices adopt v2 when convenient

1217.4 Error Handling

Use proper CoAP response codes and provide helpful error payloads:

1217.4.1 Standard Response Codes

2.01 Created              # POST created new resource
2.04 Changed              # PUT updated resource
2.05 Content              # GET successful with payload
4.00 Bad Request          # Invalid syntax
4.01 Unauthorized         # Authentication required
4.04 Not Found            # Resource doesn't exist
4.05 Method Not Allowed   # GET on write-only resource
5.00 Internal Server Error

1217.4.2 Error Payload Format

JSON for human readability:

{
  "error": {
    "code": "SENSOR_OFFLINE",
    "message": "Device has not reported in 5 minutes",
    "timestamp": "2025-01-15T10:30:00Z",
    "device_id": "temp42",
    "retry_after": 300
  }
}

CBOR for production (more efficient):

{1: "SENSOR_OFFLINE", 2: "Device offline", 3: 1642259400, 4: "temp42", 5: 300}

1217.5 Message Type Selection

Choose between CON and NON based on criticality:

Use CON (Confirmable) for: - Actuator commands (LED on/off, valve open/close) - Configuration changes - Alerts and alarms - Any operation where failure must be detected

Use NON (Non-confirmable) for: - Frequent sensor readings (temperature every minute) - Telemetry streams - Status updates - Any data where next update supersedes previous

Decision tree:

Is the data critical? -> YES -> CON
                      -> NO  -> Will data be sent again soon?
                                -> YES -> NON
                                -> NO  -> CON (don't risk losing it)

1217.6 Security Best Practices

1217.6.1 Always Use DTLS in Production

coaps://sensor.local/v1/temperature    # Secure CoAP

Authentication options: 1. Pre-Shared Key (PSK) - Simplest for constrained devices 2. Raw Public Key (RPK) - No certificate infrastructure needed 3. X.509 Certificates - Enterprise deployments

1217.6.2 Security Checklist

1217.6.3 Rate Limiting

Protect your system from misbehaving devices:

Per-device limits:

CoAP response code: 4.29 Too Many Requests

Payload (JSON):
{
  "error": "RATE_LIMIT_EXCEEDED",
  "limit": "10 requests/minute",
  "retry_after": 45
}

Implementation strategies: - Token bucket algorithm (allow bursts, limit sustained rate) - Return Max-Age option to indicate when retry is allowed - Log violations for debugging

1217.7 Worked Example: Smart Agriculture API

TipCase Study: Smart Agriculture Sensor Network

Scenario: 200 soil moisture sensors across a farm, battery-powered, reporting to a central gateway.

API Design:

# Resource structure
coap://gateway.local/v1/sensors/{sensor_id}/moisture
coap://gateway.local/v1/sensors/{sensor_id}/battery
coap://gateway.local/v1/sensors/{sensor_id}/config

# Normal operation (NON messages for efficiency)
Sensor -> Gateway: NON POST /v1/sensors/field3-42/moisture
Payload (CBOR): {value: 35, unit: "%", timestamp: 1642259400}

# Critical alerts (CON for reliability)
Sensor -> Gateway: CON POST /v1/sensors/field3-42/alert
Payload (CBOR): {type: "LOW_MOISTURE", threshold: 20, current: 15}

# Gateway observes battery levels
Gateway -> Sensor: GET /v1/sensors/field3-42/battery (Observe: 0)
Sensor -> Gateway: Notifications when battery drops below thresholds

# Versioning for future expansion
v1: Basic moisture reporting
v2: Adds soil temperature and pH
(v1 sensors continue working while v2 rolls out)

Why this works: - NON messages save battery (no ACK overhead) - CON ensures critical alerts arenโ€™t lost - CBOR minimizes bandwidth - Observe pattern prevents polling battery status - Versioning allows gradual upgrades

1217.8 Common Pitfalls

WarningCommon Pitfall: CoAP Observe Notification Flood

The mistake: Configuring a server to send Observe notifications on every minor resource change, overwhelming clients.

Symptoms: - Client device becomes unresponsive or crashes - Battery drains rapidly (constant wake-ups) - Network congestion with notification traffic - Client sends RST messages repeatedly

Why it happens: Developers bind notifications directly to sensor sampling rates (e.g., 10 Hz accelerometer) without throttling.

The fix: Implement server-side notification throttling:

# BAD: Notify on every sensor reading
@coap_resource('/temperature')
def on_read():
    current_temp = read_sensor()
    notify_observers(current_temp)  # Called 10x/second!

# GOOD: Throttle with change threshold
MIN_NOTIFY_INTERVAL = 5.0  # seconds
CHANGE_THRESHOLD = 0.5     # degrees

@coap_resource('/temperature')
def on_read():
    current_temp = read_sensor()

    should_notify = (
        abs(current_temp - last_notified_temp) >= CHANGE_THRESHOLD or
        (time.time() - last_notify_time) >= MIN_NOTIFY_INTERVAL
    )

    if should_notify:
        notify_observers(current_temp)
        last_notified_temp = current_temp
        last_notify_time = time.time()

Prevention: - Set minimum notification intervals (5-60 seconds) - Implement change thresholds (only notify on significant changes) - Use Max-Age option to tell clients how long values are valid - Monitor client RST responses (indicates overwhelmed client)

CautionPitfall: HTTP-CoAP Proxy Caching Stale Data

The Mistake: HTTP-to-CoAP proxy aggressively caches based on Max-Age without considering that freshness requirements vary by use case.

The Fix: Implement per-client cache control at the proxy:

@app.route('/coap/<path:resource>')
async def proxy_coap(resource):
    # Client-specified freshness requirement
    client_max_age = int(request.headers.get('Cache-Control', 'max-age=60').split('=')[1])

    # Check cache with client's freshness requirement
    if coap_uri in cache:
        response, cached_time, server_max_age = cache[coap_uri]
        age = time.time() - cached_time
        effective_max_age = min(client_max_age, server_max_age)

        if age < effective_max_age:
            return response.payload  # Cache hit
    # ... fetch from device if stale

Key principle: Safety-critical clients should always request fresh data (max-age=0).

1217.9 Summary

CoAP API design follows REST principles with IoT optimizations:

  • Resource design: Nouns for URIs, methods for verbs
  • Content format: CBOR for production, JSON for debugging
  • Versioning: URI-based (/v1/) for simplicity
  • Error handling: Standard response codes + helpful payloads
  • Security: Always use DTLS in production
  • Rate limiting: Protect against misbehaving devices

Key decisions: - Keep URIs short but descriptive - Use CON for commands, NON for telemetry - Implement notification throttling for Observe - Plan for multi-year device lifetimes with versioning

1217.10 Whatโ€™s Next

Now that you understand API design: