Learning Goals 5 min
Yesterday the ESP was a client (it fetched a page). Today it's a server (it answers requests). Type the ESP's IP into your laptop's browser and a webpage loads — served from your Arduino. By the end of this lesson you will:
- Use
ESP8266WebServer(orWebServeron ESP32) to handle HTTP GET requests on your ESP. - Register handlers for specific paths:
/,/led/on,/status.json. - Return both HTML and JSON responses with the right Content-Type, so a browser and a JavaScript app can each consume your endpoint.
Warm-Up 10 min
Wire-up: just the ESP and one LED with a 220 Ω resistor on D5 (NodeMCU) or any GPIO. Bring up yesterday's connection logic so you have an IP.
The big idea today
A web server is the OPPOSITE direction of a web client. The ESP listens on a TCP port (default 80) for incoming HTTP requests, parses them, runs your handler functions, and sends back a response. Same protocol as L03-31, played from the other side.
This is the most magical lesson of Cluster F: the moment you can type http://192.168.1.42/ in a browser and see a button you wired to a real LED, the line between "Arduino project" and "real internet-connected device" disappears.
New Concept · WebServer + handlers 25 min
The library
#if defined(ESP8266)
#include <ESP8266WebServer.h>
ESP8266WebServer server(80); // listen on port 80
#elif defined(ESP32)
#include <WebServer.h>
WebServer server(80);
#endifSame API on both platforms. 80 is the standard HTTP port.
Registering handlers
void handleRoot() {
server.send(200, "text/html", "<h1>Hello from Arduino</h1>");
}
void handleLedOn() {
digitalWrite(LED_PIN, HIGH);
server.send(200, "text/plain", "LED on");
}
void setup() {
// ... WiFi.begin / wait for WL_CONNECTED ...
server.on("/", handleRoot);
server.on("/led/on", handleLedOn);
server.on("/led/off", []() {
digitalWrite(LED_PIN, LOW);
server.send(200, "text/plain", "LED off");
});
server.begin();
Serial.print("# Server running. Visit http://"); Serial.println(WiFi.localIP());
}The server.on(path, fn) binds a URL path to a function. Lambdas (the []() {...} syntax) are handy for short handlers. server.send(code, contentType, body) writes the response.
The loop() must call handleClient()
void loop() {
server.handleClient(); // services pending requests
// ... other work ...
}If you forget handleClient(), the server doesn't respond. The library doesn't use interrupts — it polls. So loop() must run frequently.
Common Content-Types
| Content-Type | Use for |
|---|---|
text/html | HTML pages — what a browser renders |
text/plain | Plain text — quick status / debug responses |
application/json | JSON for API endpoints (used by JavaScript apps) |
text/css / application/javascript | If you serve static assets |
Reading query parameters
For URLs like /set?bright=128:
server.on("/set", []() {
if (server.hasArg("bright")) {
int v = server.arg("bright").toInt();
v = constrain(v, 0, 255);
analogWrite(LED_PIN, v);
server.send(200, "text/plain", "ok");
} else {
server.send(400, "text/plain", "missing ?bright=");
}
});A minimal HTML control page
server.on("/", []() {
String html = R"HTML(
<!doctype html>
<html><head><title>ESP LED</title></head>
<body>
<h1>ESP LED</h1>
<p><a href="/led/on">Turn ON</a> | <a href="/led/off">Turn OFF</a></p>
</body></html>
)HTML";
server.send(200, "text/html", html);
});The R"HTML( ... )HTML" syntax is a raw string literal — no need to escape quotes inside the HTML. Clean.
Worked Example · LED control page + JSON status 25 min
Step 1 — wire one LED to D5 (or any GPIO)
Step 2 — the sketch
// L03-32 · ESP web server — LED control via browser
#if defined(ESP8266)
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
#elif defined(ESP32)
#include <WiFi.h>
#include <WebServer.h>
WebServer server(80);
#endif
const char* SSID = "YourNetwork";
const char* PASSWORD = "YourPassword";
const int LED_PIN = 14; // D5 on NodeMCU
bool ledOn = false;
void handleRoot() {
String html = "<!doctype html><html><head>";
html += "<meta name='viewport' content='width=device-width, initial-scale=1'>";
html += "<title>ESP LED</title></head><body style='font-family:sans-serif'>";
html += "<h1>ESP LED Control</h1>";
html += "<p>Current state: <strong>";
html += (ledOn ? "ON" : "OFF");
html += "</strong></p>";
html += "<p><a href='/led/on'>Turn ON</a> | <a href='/led/off'>Turn OFF</a></p>";
html += "<p><a href='/status.json'>JSON status</a></p>";
html += "</body></html>";
server.send(200, "text/html", html);
}
void handleLedOn() {
ledOn = true;
digitalWrite(LED_PIN, HIGH);
server.sendHeader("Location", "/", true);
server.send(303); // redirect back to root
}
void handleLedOff() {
ledOn = false;
digitalWrite(LED_PIN, LOW);
server.sendHeader("Location", "/", true);
server.send(303);
}
void handleStatus() {
String json = "{\"led\":\"";
json += (ledOn ? "on" : "off");
json += "\",\"uptime\":";
json += (millis() / 1000);
json += "}";
server.send(200, "application/json", json);
}
void handleNotFound() {
server.send(404, "text/plain", "Not found");
}
void setup() {
Serial.begin(115200);
pinMode(LED_PIN, OUTPUT);
digitalWrite(LED_PIN, LOW);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) { delay(500); Serial.print("."); }
Serial.println();
Serial.print("# IP = "); Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/led/on", handleLedOn);
server.on("/led/off", handleLedOff);
server.on("/status.json", handleStatus);
server.onNotFound(handleNotFound);
server.begin();
Serial.println("# Server up.");
}
void loop() {
server.handleClient();
}Step 3 — upload + test from a browser
- Note the IP printed in Serial Monitor (e.g.
192.168.1.42). - On a laptop / phone on the SAME WiFi network, open the browser and visit
http://192.168.1.42/. - You should see the HTML page with two links.
- Click "Turn ON" — the LED lights up; the browser redirects back to
/which now shows "Current state: ON". - Click "Turn OFF" — LED off.
- Visit
http://192.168.1.42/status.json— you see the JSON response.
Step 4 — call the JSON endpoint from your laptop
In a terminal: curl http://192.168.1.42/status.json. You get the JSON. Pipe it through jq if you have it: curl ... | jq.
Step 5 — call from your laptop's browser DevTools
Open any webpage; open DevTools (F12) → Console; paste:
fetch("http://192.168.1.42/status.json").then(r => r.json()).then(console.log)
You see the parsed JSON in the console. A real JavaScript app could poll this endpoint every second and display live status on a webpage.
Step 6 — control from the phone
From your phone's browser, open http://192.168.1.42/. Same page, mobile-friendly thanks to the viewport meta tag. Tap buttons — your LED responds. You've built a tiny smart-home device.
Try It Yourself 15 min
Goal: Add a third button on the page: "Toggle". Route /led/toggle flips the LED state.
Hint
server.on("/led/toggle", []() {
ledOn = !ledOn;
digitalWrite(LED_PIN, ledOn ? HIGH : LOW);
server.sendHeader("Location", "/", true);
server.send(303);
});Goal: Brightness slider. /led/bright?value=128 sets PWM. Add a slider on the HTML page that POSTs (or GETs) to that endpoint when moved.
Hint
server.on("/led/bright", []() {
int v = constrain(server.arg("value").toInt(), 0, 255);
analogWrite(LED_PIN, v);
server.send(200, "text/plain", "ok");
});HTML side: <input type="range" min="0" max="255" oninput="fetch('/led/bright?value='+this.value)">. JavaScript in an HTML attribute — runs on every slider move and fires the GET.
Goal: Serve a real HTML page from PROGMEM (flash) so RAM isn't consumed. The page polls /status.json every second via JavaScript and updates a live display.
Hint
Use const char index_html[] PROGMEM = R"HTML(...)HTML";. In the handler: server.send_P(200, "text/html", index_html);. The _P variant reads from PROGMEM instead of expecting a RAM string.
Inside the HTML, use a setInterval(() => fetch(...).then(r => r.json()).then(...)) to poll. This is the standard "live dashboard" pattern.
Mini-Challenge · Mini API 10 min
Design an API for your device:
- List 4–6 endpoints, e.g.:
GET /status→ JSON of current statePOST /ledwith?value=128→ set brightnessGET /sensor→ current sensor readingPOST /reboot→ restart the ESP
- For each: method, path, parameters, response shape, possible errors.
- Document it like a real REST API spec.
Tomorrow we put a real sensor behind /sensor and stream readings to the browser. For L03-34 the API becomes a phone-controlled lamp. Designing the API upfront makes the implementation easy.
Recap 5 min
The ESP is now a server. WebServer server(80), server.on(path, fn), server.handleClient() in loop(). Returns HTML for browsers, JSON for JavaScript. Live on your local network at http://<esp-IP>/. Only accessible from the same WiFi — public reach needs port forwarding or a tunnel. Tomorrow we wire a sensor and serve its live readings to the browser.
- Web server
- A program that listens on a TCP port (usually 80) for HTTP requests and returns responses. Apache, nginx, and your ESP all do the same job.
- Endpoint / route
- A specific URL path that the server handles.
/statusand/led/onare two endpoints. - Handler
- The function bound to a route. Runs when a request to that route arrives. Generates the response.
server.handleClient()- The poll call that services pending requests. Must be in
loop(). - Content-Type
- The HTTP header that tells the client how to interpret the body.
text/html,application/json, etc. - HTTP method (GET / POST / PUT / DELETE)
- The verb of the request.
server.on(path, HTTP_GET, fn)binds a method-specific handler. - Query parameter
- The
?key=valuebit on a URL. Read withserver.arg("key"). - HTTP 303 (See Other)
- A redirect response that tells the browser to fetch a different URL with GET. Used after form submissions to prevent the "refresh sends the form again" problem.
- PROGMEM
- An AVR / ESP keyword that stores a constant in flash instead of RAM. Use for big HTML pages.
Homework 5 min
- Get the LED control page working from your phone or laptop's browser. Record a 30-second screen capture: page → ON → page updates → OFF.
- Save as
esp-web-led.ino. - Bring tomorrow: any analog sensor (LDR / pot / TMP36). We'll stream its reading to a browser dashboard.
- Read ahead to ARD-L03-33 (Sensor Data Over WiFi).
Bring back next class:
- Working web-server sketch + screen capture.
- An analog sensor for L03-33.