ESP32 Web Services Guide: WebServer, mDNS and REST APIs
L2 finale: Wi-Fi First Steps made the device reach the network; this guide makes it reachable — open a page in a phone browser, tap a button, control the LED. It’s the complete answer to the L2 acceptance project and the prototype for smart-home dashboards and web config UIs.
1. WebServer in three minutes
#include <WiFi.h>
#include <WebServer.h>
WebServer server(80); // port 80
void handleRoot() {
server.send(200, "text/html", "<h1>ESPDIY</h1><a href='/on'>LED ON</a> <a href='/off'>LED OFF</a>");
}
void handleOn() { digitalWrite(2, HIGH); server.sendHeader("Location", "/"); server.send(303); }
void handleOff() { digitalWrite(2, LOW); server.sendHeader("Location", "/"); server.send(303); }
void setup() {
pinMode(2, OUTPUT);
WiFi.begin("YOUR_SSID", "YOUR_PASSWORD");
while (WiFi.status() != WL_CONNECTED) delay(300);
server.on("/", handleRoot); // register routes
server.on("/on", handleOn);
server.on("/off", handleOff);
server.begin();
Serial.printf("Visit: http://%s\n", WiFi.localIP().toString().c_str());
}
void loop() {
server.handleClient(); // must be called continuously (non-blocking)
}
Open the printed IP in a browser → tap “LED ON” → the LED lights. Core mechanism: route registration + handleClient polled in loop (continuing the non-blocking paradigm — each request takes milliseconds and disturbs nothing).
2. mDNS: forget the IP
IPs are forgettable and change. mDNS lets LAN devices address each other as espdiy.local (natively supported by phones and PCs):
#include <ESPmDNS.h>
void setup() {
// ... after Wi-Fi connects:
if (MDNS.begin("espdiy")) { // hostname = espdiy.local
Serial.println("mDNS started: http://espdiy.local");
}
MDNS.addService("http", "tcp", 80); // advertise the HTTP service
}
Type
http://espdiy.localin the phone browser. One name per device — production units use serial-number suffixes (espdiy-a3f2) to avoid collisions. Limits: same LAN only; some Android browsers handle .local poorly — keep the IP as a fallback.
3. Advanced: REST API + AJAX (the modern way)
Full-page links are “web 1.0”. The IoT-standard architecture: static page hosting + REST JSON endpoints + AJAX partial refresh:
// REST: GET /api/status returns the status JSON
void handleApiStatus() {
JsonDocument doc;
doc["led"] = digitalRead(2) == HIGH;
doc["uptime"] = millis() / 1000;
String out;
serializeJson(doc, out);
server.send(200, "application/json", out);
}
// REST: POST /api/led toggles the LED
void handleApiLed() {
digitalWrite(2, !digitalRead(2));
handleApiStatus(); // return the new state
}
void setup() {
// ...
server.on("/api/status", HTTP_GET, handleApiStatus);
server.on("/api/led", HTTP_POST, handleApiLed);
}
The page (as a string constant or a SPIFFS file) calls the API with fetch:
<button onclick="fetch('/api/led',{method:'POST'}).then(refresh)">Toggle LED</button>
<span id="status"></span>
<script>
async function refresh() {
const s = await (await fetch('/api/status')).json();
document.getElementById('status').textContent =
(s.led ? 'ON' : 'OFF') + ' · up ' + s.uptime + 's';
}
setInterval(refresh, 2000); // refresh every 2s
</script>
This architecture = the prototype of Home Assistant dashboards and the WLED web UI — L3’s ESPAsyncWebServer is its high-performance upgrade.
4. The L2 acceptance project: full chain
Now string together all of L2 for the self-check:
[Wi-Fi] event-driven connect + auto-reconnect
↓
[mDNS] espdiy.local entry point
↓
[WebServer] page + REST API
↓
[PWM/timers/interrupts/ADC/I2C] business logic (breathing LED, sensor polling, buttons)
↓
Phone browser → control & monitor
Full chain working = L2 graduation 🎓
5. Pitfall cheat sheet
| Symptom | Cause and fix |
|---|---|
| Page never loads | server.handleClient() missing from loop |
| mDNS doesn’t resolve | Phone on 5GHz while the ESP32 is on 2.4GHz (AP isolation on some routers); verify via IP |
| POST body “missing” | Body isn’t parsed by default — use server.arg("xx") with the right route method |
| Page gets sluggish | HTML strings eat RAM (PROGMEM / LittleFS); or move to AsyncWebServer |
| Intermittent response | A long delay in loop blocks handleClient (revisit timers) |
| espdiy.local collision | Use serial-number suffixes in production |
| Wanting HTTPS | Cloudflare reverse proxy for static sites; local certs are an L4 topic |
6. Further reading
- 🧭 Previous: Wi-Fi First Steps — the client side
- ⏭️ L3 directions: ESPAsyncWebServer (async performance), MQTT (IoT protocol), ESP-NOW (routerless comms)
- 🧭 Tool tie-in: the WLED firmware is the industrial-grade version of this architecture — excellent reference reading
License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com