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.local in 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

SymptomCause and fix
Page never loadsserver.handleClient() missing from loop
mDNS doesn’t resolvePhone 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 sluggishHTML strings eat RAM (PROGMEM / LittleFS); or move to AsyncWebServer
Intermittent responseA long delay in loop blocks handleClient (revisit timers)
espdiy.local collisionUse serial-number suffixes in production
Wanting HTTPSCloudflare reverse proxy for static sites; local certs are an L4 topic

6. Further reading


License: CC BY-SA 4.0. Repost & remix with attribution: ESPDIY Maker Lab - espdiy.com