1// Copyright (C) 2026 Douglas Quigg (dstroy0) <dquigg123@gmail.com>
2// SPDX-License-Identifier: AGPL-3.0-or-later
6 * @brief Advanced example demonstrating RESTful CRUD APIs, header verification, and zero-allocation JSON parsing.
8 * This example showcases:
9 * 1. RESTful CRUD endpoints (GET, POST, PATCH, DELETE) for a mock sensor database.
10 * 2. Strict HTTP request validation:
11 * - Authorization headers (bearer token) verified before modifications.
12 * - Content-Type header checked for incoming POST/PATCH requests.
13 * 3. Extraction of wildcard ID parameters from routes (e.g., /api/sensors/2).
14 * 4. Zero-heap manual JSON scanning for incoming payloads.
15 * 5. Response diversity using different HTTP status codes:
22 * 6. Query string filtering based on parameter values.
24 * To run this example:
25 * - Configure SSID/PASSWORD, and upload to an ESP32.
26 * - Use a REST client (like Postman or curl) to interact with /api/sensors.
29#include "DeterministicESPAsyncWebServer.h"
30#include "network_drivers/physical.h"
33static const char *SSID = "YOUR_SSID";
34static const char *PASSWORD = "YOUR_PASSWORD";
38// Security token required for write/delete actions
39static const char *EXPECTED_TOKEN = "Bearer secret_admin_token";
41// Zero-allocation static "database" model
48 bool in_use; // Slot flag
52static SensorDevice sensor_db[MAX_SENSORS] = {
53 {0, "Living Room", 22.4, true, true},
54 {1, "Kitchen", 24.1, true, true},
55 {2, "Garage", 15.8, false, true},
56 {3, "", 0.0, false, false} // Empty slot
59// --- Helper Functions ---
62 * @brief Checks if a request has a valid Authorization header.
63 * @return true if authenticated, false otherwise.
65bool is_authorized(const HttpReq *req)
67 const char *auth_hdr = http_get_header(req, "Authorization");
68 if (auth_hdr && strcmp(auth_hdr, EXPECTED_TOKEN) == 0)
76 * @brief Custom helper to scan float values out of a simple JSON body.
77 * Works without heap allocation by scanning the buffer.
79bool json_get_float(const char *json, const char *key, float &out_val)
81 // Search for key pattern, e.g., "temp":
83 snprintf(key_pattern, sizeof(key_pattern), "\"%s\":", key);
85 const char *ptr = strstr(json, key_pattern);
89 // Move pointer past key
90 ptr += strlen(key_pattern);
93 while (*ptr == ' ' || *ptr == '\t')
96 out_val = (float)atof(ptr);
101 * @brief Custom helper to scan string values out of a simple JSON body.
103bool json_get_string(const char *json, const char *key, char *out_buf, size_t max_len)
105 char key_pattern[32];
106 snprintf(key_pattern, sizeof(key_pattern), "\"%s\":", key);
108 const char *ptr = strstr(json, key_pattern);
112 ptr += strlen(key_pattern);
114 // Skip spaces or quotes
115 while (*ptr == ' ' || *ptr == '\t' || *ptr == '"')
119 while (*ptr && *ptr != '"' && *ptr != ',' && *ptr != '}' && i < (max_len - 1))
121 out_buf[i++] = *ptr++;
128 * @brief Custom helper to scan boolean values out of a simple JSON body.
130bool json_get_bool(const char *json, const char *key, bool &out_val)
132 char key_pattern[32];
133 snprintf(key_pattern, sizeof(key_pattern), "\"%s\":", key);
135 const char *ptr = strstr(json, key_pattern);
139 ptr += strlen(key_pattern);
140 while (*ptr == ' ' || *ptr == '\t')
143 if (strncmp(ptr, "true", 4) == 0)
148 else if (strncmp(ptr, "false", 5) == 0)
156// --- Route Handlers ---
159 * @brief GET /api/sensors
160 * Serves list of sensors. Supports query filter: ?active=1 or ?active=0
162void handle_get_sensors(uint8_t slot_id, HttpReq *req)
164 const char *active_filter = http_get_query(req, "active");
165 bool filter_by_active = (active_filter != nullptr);
166 bool active_target_val = (filter_by_active && strcmp(active_filter, "1") == 0);
168 // Build the JSON list response. We construct this incrementally on stack.
169 char response_buf[512];
170 int len = snprintf(response_buf, sizeof(response_buf), "[");
173 for (int i = 0; i < MAX_SENSORS; i++)
175 if (!sensor_db[i].in_use)
177 if (filter_by_active && (sensor_db[i].active != active_target_val))
182 len += snprintf(response_buf + len, sizeof(response_buf) - len, ",");
186 len += snprintf(response_buf + len, sizeof(response_buf) - len,
187 "{\"id\":%d,\"name\":\"%s\",\"temp\":%.1f,\"active\":%s}", sensor_db[i].id, sensor_db[i].name,
188 sensor_db[i].temperature, sensor_db[i].active ? "true" : "false");
190 snprintf(response_buf + len, sizeof(response_buf) - len, "]");
192 server.send(slot_id, 200, "application/json", response_buf);
196 * @brief GET /api/sensors/* (wildcard match)
197 * Extracts sensor ID from the path prefix /api/sensors/
199void handle_get_sensor_by_id(uint8_t slot_id, HttpReq *req)
201 // Length of "/api/sensors/" is 13
202 if (strlen(req->path) <= 13)
204 server.send(slot_id, 400, "text/plain", "Missing sensor ID");
208 int id = atoi(req->path + 13);
209 if (id < 0 || id >= MAX_SENSORS || !sensor_db[id].in_use)
211 server.send(slot_id, 404, "application/json", "{\"error\":\"Sensor not found\"}");
215 char response_buf[192];
216 snprintf(response_buf, sizeof(response_buf), "{\"id\":%d,\"name\":\"%s\",\"temp\":%.1f,\"active\":%s}",
217 sensor_db[id].id, sensor_db[id].name, sensor_db[id].temperature, sensor_db[id].active ? "true" : "false");
219 server.send(slot_id, 200, "application/json", response_buf);
223 * @brief POST /api/sensors
224 * Creates a new sensor. Requires authentication and JSON payload.
226void handle_create_sensor(uint8_t slot_id, HttpReq *req)
228 // 1. Verify Authorization
229 if (!is_authorized(req))
231 server.send(slot_id, 401, "text/plain", "401 Unauthorized: Invalid token");
235 // 2. Verify Content-Type
236 const char *content_type = http_get_header(req, "Content-Type");
237 if (!content_type || strstr(content_type, "application/json") == nullptr)
239 server.send(slot_id, 400, "text/plain", "400 Bad Request: Content-Type must be application/json");
243 // Find an empty slot
245 for (int i = 0; i < MAX_SENSORS; i++)
247 if (!sensor_db[i].in_use)
254 if (empty_slot == -1)
256 server.send(slot_id, 409, "application/json", "{\"error\":\"Database table full\"}");
260 // 3. Extract JSON keys
261 const char *body = (const char *)req->body;
266 if (!json_get_string(body, "name", name, sizeof(name)) || !json_get_float(body, "temp", temp) ||
267 !json_get_bool(body, "active", active))
269 server.send(slot_id, 400, "application/json", "{\"error\":\"Invalid JSON format or missing keys\"}");
274 sensor_db[empty_slot].id = empty_slot;
275 strncpy(sensor_db[empty_slot].name, name, sizeof(sensor_db[empty_slot].name) - 1);
276 sensor_db[empty_slot].temperature = temp;
277 sensor_db[empty_slot].active = active;
278 sensor_db[empty_slot].in_use = true;
280 char response_buf[192];
281 snprintf(response_buf, sizeof(response_buf), "{\"id\":%d,\"name\":\"%s\",\"status\":\"created\"}", empty_slot,
282 sensor_db[empty_slot].name);
284 server.send(slot_id, 201, "application/json", response_buf);
288 * @brief PATCH /api/sensors/*
289 * Partially updates a sensor's temperature or activity state. Requires authentication.
291void handle_patch_sensor(uint8_t slot_id, HttpReq *req)
293 if (!is_authorized(req))
295 server.send(slot_id, 401, "text/plain", "401 Unauthorized: Invalid token");
299 if (strlen(req->path) <= 13)
301 server.send(slot_id, 400, "text/plain", "Missing sensor ID");
305 int id = atoi(req->path + 13);
306 if (id < 0 || id >= MAX_SENSORS || !sensor_db[id].in_use)
308 server.send(slot_id, 404, "application/json", "{\"error\":\"Sensor not found\"}");
312 const char *body = (const char *)req->body;
316 if (json_get_float(body, "temp", new_temp))
318 sensor_db[id].temperature = new_temp;
320 if (json_get_bool(body, "active", new_active))
322 sensor_db[id].active = new_active;
325 char response_buf[192];
326 snprintf(response_buf, sizeof(response_buf), "{\"id\":%d,\"name\":\"%s\",\"temp\":%.1f,\"active\":%s}",
327 sensor_db[id].id, sensor_db[id].name, sensor_db[id].temperature, sensor_db[id].active ? "true" : "false");
329 server.send(slot_id, 200, "application/json", response_buf);
333 * @brief DELETE /api/sensors/*
334 * Removes a sensor from the database. Requires authentication.
336void handle_delete_sensor(uint8_t slot_id, HttpReq *req)
338 if (!is_authorized(req))
340 server.send(slot_id, 401, "text/plain", "401 Unauthorized: Invalid token");
344 if (strlen(req->path) <= 13)
346 server.send(slot_id, 400, "text/plain", "Missing sensor ID");
350 int id = atoi(req->path + 13);
351 if (id < 0 || id >= MAX_SENSORS || !sensor_db[id].in_use)
353 server.send(slot_id, 404, "application/json", "{\"error\":\"Sensor not found\"}");
357 // Free slot in database
358 sensor_db[id].in_use = false;
360 // 204 status requires no response body
361 server.send_empty(slot_id, 204);
366 Serial.begin(115200);
368 Serial.println("\n--- DetWebServer Advanced REST CRUD Example ---");
370 init_wifi_physical(SSID, PASSWORD);
371 while (!wifi_ready())
376 Serial.println("\nWiFi Associated!");
377 Serial.print("Local IP: ");
378 Serial.println(WiFi.localIP());
380 server.set_cors("*");
382 // Map REST routes using methods
383 server.on("/api/sensors", HTTP_GET, handle_get_sensors);
384 server.on("/api/sensors/*", HTTP_GET, handle_get_sensor_by_id);
385 server.on("/api/sensors", HTTP_POST, handle_create_sensor);
386 server.on("/api/sensors/*", HTTP_PATCH, handle_patch_sensor);
387 server.on("/api/sensors/*", HTTP_DELETE, handle_delete_sensor);
389 if (server.begin(80))
391 Serial.println("REST API Server running on port 80");
392 Serial.println("Admin token expected in headers: 'Authorization: Bearer secret_admin_token'");