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)
D5 is GPIO 14, not the UNO's pin 5 — wire by the printing on the board and let the sketch worry about the number. The ESP runs at 3.3 V, so 220 Ω gives a dimmer LED here than on an UNO, which is fine and expected.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.
Basic 5 min
Goal: Add a third button on the page: "Toggle". Route /led/toggle flips the LED state.
Challenge 1 5 min
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.
Challenge 2 5 min
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.
Challenge 3 · Fix the silent server 10 min
A classmate cut the Worked Example down to one page and one LED route. It compiles and connects to WiFi. Then three things go wrong:
- (a) The browser shows raw tags like
<h1>instead of a heading. - (b) Clicking "Turn ON" leaves the browser spinning, and the LED stays dark.
- (c) After a minute, the page will not load at all.
#include <ESP8266WiFi.h>
#include <ESP8266WebServer.h>
ESP8266WebServer server(80);
const char* SSID = "YourNetwork";
const char* PASSWORD = "YourPassword";
const int LED_PIN = 14;
void handleRoot() {
server.send(200, "text/plain", "<h1>ESP LED</h1><a href='/led/on'>Turn ON</a>");
}
void handleLedOn() {
digitalWrite(LED_PIN, HIGH);
}
void setup() {
Serial.begin(115200);
WiFi.mode(WIFI_STA);
WiFi.begin(SSID, PASSWORD);
while (WiFi.status() != WL_CONNECTED) {
delay(500);
}
Serial.println(WiFi.localIP());
server.on("/", handleRoot);
server.on("/led/on", handleLedOn);
server.begin();
}
void loop() {
delay(1000);
}There are four mistakes. For each one, say which symptom it causes and write the fix. Then upload the fixed sketch and test it from a browser.
It works if the page shows a real heading, "Turn ON" lights the LED, and the browser returns to the page straight away.
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.
Extra Mission 5 min
Part 1 — Design an API for your own device
Pick a device you would like to control from a browser: a desk lamp, a pet feeder, a room fan, a scoreboard. On paper, design its web API like a real REST spec.
Your design must include:
- 4–6 endpoints, for example
GET /statusorGET /led?value=128. - For each one: the method, path, parameters and what it sends back.
- At least one endpoint that returns JSON.
- The error each endpoint gives for a missing or bad parameter, with its status code (400 or 404).
Part 2 — Make it
Build your API on the ESP with LEDs standing in for the device's outputs. Every endpoint in your design must answer. Bad parameters must get your error code, and unknown paths must get 404.
Bring back next class: your API sheet, your uploaded sketch, and a screenshot of each endpoint answering in a browser. Also bring an analog sensor (LDR, pot or TMP36) for L03-33.