Create an ESP32 Web Server to Control Devices Over WiFi ๐
Today, weโll create a web server on the ESP32 that displays a control interface with two buttons. When you click these buttons from a browser, the ESP32 will toggle the corresponding outputs, controlling LEDs or replace them with relays to control highโvoltage devices like lights, fans, or sockets โก
๐ง Before We Begin:ย If youโre completely new to the ESP32, I recommend checking out my previous blog post, anย Ultimate ESP32 Beginnerโs Guide. Weโve covered the basics of setting up your development environment, installing board support, and uploading your first program.
What Youโll Need?
Hardware Components
- ESP32 Development Boardย (any variant: ESP32 DevKit, NodeMCU-32S, etc.)
- 2 LEDs (for testing) & 2 ร 220ฮฉ resistors
- Breadboardย (optional but recommended)
- Jumper Wiresย (male-to-female or male-to-male)
- Micro-USB Cableย for programming and power
- (Optional) 2โChannel Relay Module

๐งฐ I highly recommend getting a complete ESP32ย starter kit.
It will save you time, money, and frustrationโand help you learn IoT much faster.
Software Requirements
- Arduino IDEย or VS Code & PlatformIO
- ESP32 Board Support Packageย
Note: If you havenโt set up your Arduino IDE with ESP32 support yet, please followย my previous ESP32 setup guideย before proceeding.
๐ Circuit Connections

| Device | ESP32 GPIO |
|---|---|
| LED 1 | GPIO 16 |
| LED 2 | GPIO 17 |
- LED anode โ GPIO pin (through 220ฮฉ resistor)
- LED cathode โ GND
โ ๏ธ GPIO PINs can be changed in the code.

๐ป ESP32 Web Server Code (Access Point)
Copy and paste the following code into the Arduino IDE:
// Load Wi-Fi library
#include <WiFi.h>
// Network credentials Here
const char* ssid = "ESP32-Network";
const char* password = "Esp32-Password";
// Set web server port number to 80
WiFiServer server(80);
// Variable to store the HTTP request
String header;
//variables to store the current LED states
String statePin16 = "off";
String statePin17 = "off";
//Output variable to GPIO pins
const int ledPin16 = 16;
const int ledPin17 = 17;
// Current time
unsigned long currentTime = millis();
// Previous time
unsigned long previousTime = 0;
// Define timeout time in milliseconds
const long timeoutTime = 2000;
void setup() {
Serial.begin(115200);
pinMode(ledPin16, OUTPUT); // set the LED pin mode
digitalWrite(ledPin16, 0); // turn LED off by default
pinMode(ledPin17, OUTPUT); // set the LED pin mode
digitalWrite(ledPin17, 0); // turn LED off by default
WiFi.softAP(ssid,password);
// Print IP address and start web server
Serial.println("");
Serial.println("IP address: ");
Serial.println(WiFi.softAPIP());
server.begin();
}
void loop() {
WiFiClient client = server.available(); // Listen for incoming clients
if (client) { // If a new client connects,
currentTime = millis();
previousTime = currentTime;
Serial.println("New Client."); // print a message out in the serial port
String currentLine = ""; // make a String to hold incoming data from the client
while (client.connected() && currentTime - previousTime <= timeoutTime) {
// loop while the client's connected
currentTime = millis();
if (client.available()) { // if there's bytes to read from the client,
char c = client.read(); // read a byte, then
Serial.write(c); // print it out the serial monitor
header += c;
if (c == '\n') { // if the byte is a newline character
// if the current line is blank, you got two newline characters in a row.
// that's the end of the client HTTP request, so send a response:
if (currentLine.length() == 0) {
// HTTP headers always start with a response code (e.g. HTTP/1.1 200 OK)
// and a content-type so the client knows what's coming, then a blank line:
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println();
// turns the GPIOs on and off
if (header.indexOf("GET /16/on") >= 0) {
statePin16 = "on";
digitalWrite(ledPin16, HIGH); // turns the LED on
} else if (header.indexOf("GET /16/off") >= 0) {
statePin16 = "off";
digitalWrite(ledPin16, LOW); //turns the LED off
}
if (header.indexOf("GET /17/on") >= 0) {
statePin17 = "on";
digitalWrite(ledPin17, HIGH); // turns the LED on
} else if (header.indexOf("GET /17/off") >= 0) {
statePin17 = "off";
digitalWrite(ledPin17, LOW); //turns the LED off
}
// Display the HTML web page
client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">");
// CSS to style the on/off buttons
client.println("<style>html { font-family: monospace; display: inline-block; margin: 0px auto; text-align: center;}");
client.println(".button { background-color: yellowgreen; border: none; color: white; padding: 16px 40px;");
client.println("text-decoration: none; font-size: 32px; margin: 2px; cursor: pointer;}");
client.println(".button2 {background-color: gray;}</style></head>");
client.println("<body><h1>ESP32 Web Server</h1>");
client.println("<p>Control LED State</p>");
if (statePin16 == "off") {
client.println("<p><a href=\"/16/on\"><button class=\"button\">ON</button></a></p>");
} else {
client.println("<p><a href=\"/16/off\"><button class=\"button button2\">OFF</button></a></p>");
}
if (statePin17 == "off") {
client.println("<p><a href=\"/17/on\"><button class=\"button\">ON</button></a></p>");
} else {
client.println("<p><a href=\"/17/off\"><button class=\"button button2\">OFF</button></a></p>");
}
client.println("</body></html>");
// The HTTP response ends with another blank line
client.println();
// Break out of the while loop
break;
} else { // if you got a newline, then clear currentLine
currentLine = "";
}
} else if (c != '\r') { // if you got anything else but a carriage return character,
currentLine += c; // add it to the end of the currentLine
}
}
}
// Clear the header variable
header = "";
// Close the connection
client.stop();
Serial.println("Client disconnected.");
Serial.println("");
}
}
In this project, the ESP32 acts as a WiFi Access Point:
- WiFi Name (SSID):
ESP32-Network - Password:
Esp32-Password - Default IP Address:
192.168.4.1

โถ๏ธ Upload & Test
- Select your ESP32 board and COM port
- Upload the code using the (Right Arrow icon)
- Open the Serial Monitor (Tools โ Serial Monitor) (baud rate 115200)
- Copy the ESP32 IP address 192.168.4.1
- Connect your phone/PC to the
ESP32-Network(Password:Esp32-Password) - Paste the IP address 192.168.4.1 into your browser
๐ You should now see two buttons controlling the LEDs!

โ๏ธ Code Explanation (In details):
โ This code turns yourย ESP32 into a web server. We can connect directly to it using a smartphone or laptop โ no router and no internet required.
1๏ธโฃย Libraries and Network Configuration
#include <WiFi.h>
const char* ssid = "ESP32-Network";
const char* password = "Esp32-Password";
- We use the โWiFi.hโ library, which enables our ESP32 WiFi features.
- Then we create an access point named โESP32-Networkโ with a password โEsp32-Passwordโ
- You can change these credentials to secure access to the web server.
2๏ธโฃ Web Server Setup
WiFiServer server(80); String header;
- This creates a server listening on port 80 (standard HTTP port)
- The
headerย variable stores the complete HTTP request from clients
3๏ธโฃย LED State Variables
String statePin16 = "off"; String statePin17 = "off"; const int ledPin16 = 16; const int ledPin17 = 17;
- This code tracks LED states as strings (โonโ/โoffโ)
- Defines which GPIO pins control the LEDs (16,17)
4๏ธโฃย Timeout Mechanism
unsigned long currentTime = millis(); unsigned long previousTime = 0; const long timeoutTime = 2000;
millis(): Gets time since ESP32 started (in milliseconds)timeoutTime = 2000: Client connection times out after 2 seconds- Prevents hanging connections
5๏ธโฃย Setup Function
void setup() {
Serial.begin(115200);
pinMode(ledPin16, OUTPUT);
digitalWrite(ledPin16, 0);
pinMode(ledPin17, OUTPUT);
digitalWrite(ledPin17, 0);
WiFi.softAP(ssid,password);
Serial.println("");
Serial.println("IP address: ");
Serial.println(WiFi.softAPIP());
server.begin();
} - Initialize Serial communication (115200 baud)
- Configure pins 16 & 17 as OUTPUT, set to LOW (LEDs off)
- Start WiFi Access Point:ย
WiFi.softAP(ssid,password) - Print AP IP address (usually 192.168.4.1)
- Start web server:ย
server.begin()
6๏ธโฃย Main Loop โ Client Handling
void loop() {
WiFiClient client = server.available(); // Check for new clients
if (client) { // If client connects
currentTime = millis();
previousTime = currentTime;
Serial.println("New Client.");
String currentLine = ""; server.available(): Checks for incoming client connectionsif (client): True when a client connects- Resets timeout timer
currentLine: Stores the current line of the HTTP request
7๏ธโฃย HTTP Request Processing Loop
while (client.connected() && currentTime - previousTime <= timeoutTime) {
currentTime = millis();
if (client.available()) {
char c = client.read();
Serial.write(c);
header += c; - Main loop runs while client is connected and hasnโt timed out
client.read(): Reads one character from the clientSerial.write(c): Echoes character to Serial Monitor (debugging)header += c: Appends character to complete request
8๏ธโฃย End of Request Detection
if (c == '\n') {
if (currentLine.length() == 0) {
// End of HTTP request detected - HTTP requests end with two consecutive newlines (
\n) currentLine.length() == 0ย means empty line after header- Time to process the request and send response
9๏ธโฃย Sending HTTP Response Headers
client.println("HTTP/1.1 200 OK");
client.println("Content-type:text/html");
client.println("Connection: close");
client.println(); HTTP Response Structure:
- Status line:ย
HTTP/1.1 200 OKย (success) - Content type:ย
text/htmlย (sending HTML) - Connection:ย
closeย (close after response) - Blank line (separates headers from content)
๐ย Parsing LED Control Commands
if (header.indexOf("GET /16/on") >= 0) {
statePin16 = "on";
digitalWrite(ledPin16, HIGH);
} else if (header.indexOf("GET /16/off") >= 0) {
statePin16 = "off";
digitalWrite(ledPin16, LOW);
} - We use
indexOf()ย to search for patterns in the HTTP request - Then we check if the header contains specific GET requests:
/16/onย โ Turn LED on pin 16 ON/16/offย โ Turn LED on pin 16 OFF
1๏ธโฃ1๏ธโฃย Generating HTML Page
client.println("<!DOCTYPE html><html>");
client.println("<head><meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">");
client.println("<link rel=\"icon\" href=\"data:,\">"); This sends an HTML document line by line
1๏ธโฃ2๏ธโฃย Dynamic Button Creation
if (statePin16 == "off") {
client.println("<p><a href=\"/16/on\"><button class=\"button\">ON</button></a></p>");
} else {
client.println("<p><a href=\"/16/off\"><button class=\"button button2\">OFF</button></a></p>");
} Logic:
- If LED is OFF โ Show green โONโ button that links toย
/16/on - If LED is ON โ Show gray โOFFโ button that links toย
/16/off - This creates a toggle mechanism for each device
1๏ธโฃ3๏ธโฃย End of Response and Cleanup
client.println("</body></html>");
client.println();
break; - Closes HTML tags
- Adds final blank line (end of HTTP response)
break: Exits the while loop
1๏ธโฃ4๏ธโฃย Client Disconnection
header = "";
client.stop();
Serial.println("Client disconnected."); - Finally, we Clear header for the next client
- Then it closes the client connection
- And prints a disconnect message
โกUsing a Relay (High Voltage Control)
To control real devices, you simply have to :
- Replace the LED with a relay module
- GPIO 16 โ Relay IN
- Relay VCC โ 5V / 3.3V (check module)
- Relay GND โ ESP32 GND

โ ๏ธ Warning: High voltage is dangerous
The code stays the same ๐
Whatโs Next: Taking Your Project Further
Now that you have a working ESP32 web server, here are several directions you can take your project:
Multiple Client Support:
Modify your code to handle multiple simultaneous connections using non-blocking techniques or the WebServer library.
Transform your ESP32 microcontroller into a real-time temperature and humidity monitoring station that serves data through a web interface.ย
Integrate with Alexa or Google Assistant to create aย voice-controlled smart lampย using an ESP32 microcontroller and an Amazon Echo Dot.

