ESP-IDF: ESP32 Web Server Access Point (AP) Mode

This guide shows how to build a local web server in Access Point (AP) mode with the ESP32 programmed with ESP-IDF that serves a simple HTML web page. This web page can be accessed when you are connected directly to the ESP32 Access Point.

ESP-IDF ESP32 Web Server Access Point AP Mode

Using Arduino IDE? Follow this tutorial instead: Building an ESP32 Web Server: The Complete Guide for Beginners (Arduino IDE).

Prerequisites

Before following this guide, you need to install the ESP-IDF extension on VS Code IDE (Microsoft Visual Studio Code). Follow the next guide to install it, if you haven’t already:

You will also need an ESP32 development board model of your choice.

Introducing Web Servers

In simple terms, a web server is a “computer” that delivers web pages. It stores the website’s files, including HTML documents and related assets like images, CSS style sheets, fonts, and other files. When a user makes a request, the server sends those files to the user’s web browser.

When you access a web page in your browser, you’re actually sending a request to a server using HTTP. This protocol handles how information is requested and delivered on the Internet. The server then responds by sending the web page you asked for, also through HTTP.

ESP-IDF: ESP32 as an Access Point (AP)

When your ESP32 is set up as an Access Point (AP), other devices (such as your smartphone, tablet, or computer) can connect to it without the need for a router; the ESP32 controls its own Wi-Fi network.

ESP32 Access Point

Unlike a router, an ESP32 Access Point doesn’t connect further to a wired network or the Internet, so you can’t access external libraries, publish sensor readings to the cloud, or use services like mail.

Creating an ESP-IDF Template App Project for the ESP32

The ESP-IDF extension provides an easy way to create a project from scratch with all the required files and configurations generated automatically.

To create a new ESP-IDF project on VS Code, follow these steps:

  1. Open the ESP-IDF Espressif extension and expand the “Advanced” menu
  2. Click the “New Project Wizard” option
  3. Choose the “Use ESP-IDF v6.X” to select the ESP-IDF framework version
VS Code Open ESP-IDF Extension New Project Wizard Select ESP-IDF Version

In the menu, select the “ESP-IDF Templates” sample project and press the “Create project using template sample project” button.

ESP-IDF ESP32 Create New Sample Project using IDF Template

A new window opens, you need to fill in these fields:

  • Project Name: type the desired project name;
  • Enter Project Directory: click the folder icon and select the target folder to save all your project files. You can use any directory. Note: do NOT use a Google Drive / One Drive / Dropbox folder, because it will write/create many files during the building process—if it’s on a cloud folder, this process might be extremely slow;
  • ESP-IDF Target: select the target device chip, I’m using an ESP32 with the esp32s3 chip;
  • ESP-IDF Board: for the esp32s3 chip, I also need to select the configuration: ESP32-S chip (via builtin USB-JTAG);
  • Serial Port: while having your ESP32 board connected to your computer, select the correct COM port number that refers to your ESP32;
  • Choose Template: click the blue button to create a new project using a template.
ESP-IDF ESP32 Create Open New Project Wizard Menu Select Directory Board Template

Opening the ESP-IDF Project on VS Code

After a few seconds, a notification will appear in a new window in VS Code. You can click “Open Project” to open the newly created ESP-IDF sample project template.

ESP-IDF ESP32 Open New Project Sample

IMPORTANT: if you didn’t see the notification that allows you to automatically open the ESP-IDF project on VS Code, you can easily do it by following these instructions:

Go to File > Open Folder…

ESP-IDF ESP32 Open Project Folder VS Code File Menu

Browse on your computer for the esp-idf-project folder (your project folder name that you’ve previously defined) and “Select Folder“.

ESP-IDF ESP32 Open Project VS Code Select Folder

That’s it! Your new ESP-IDF project template has been successfully created and opened.

ESP-IDF generates many files, folders, and subfolders for your project. For this guide, I recommend keeping all the default files unchanged; we will only modify the main.c file.

The example code will be written in the main.c file. To open it, follow these instructions:

  1. Open the project explorer by clicking the first icon on the left sidebar.
  2. Select your project folder name, in my case it’s “ESP-IDF-PROJECT“.
  3. Expand the “main” folder.
  4. Click the “main.c” file.
  5. The default main.c template file loads in the code window.
ESP-IDF ESP32 Open Project in a VS Code Browse to Main C File

Code: ESP32 Access Point (AP) Mode using ESP-IDF

Copy the following code to the main.c file. This code serves a basic HTML web page hosted on an ESP32 with some text content while the ESP32 is set as an Access Point.

/*  
  Rui Santos & Sara Santos - Random Nerd Tutorials
  https://RandomNerdTutorials.com/esp-idf-esp32-web-server-access-point/
*/
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "esp_mac.h"
#include "sdkconfig.h"

// Replace with your Access Point (AP) SSID and Password
#define MY_ESP_WIFI_SSID      "ESP32_AP"
#define MY_ESP_WIFI_PASS      "password123"

static const char *TAG = "web_server_ap";

// Global AP netif handle
static esp_netif_t *ap_netif;

// HTML web page to serve 
static const char *html_page = 
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP-IDF: Web Server (Access Point)</title>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
    "</head>"
    "<body>"
    "<h1>ESP-IDF: Web Server (Access Point)</h1>"
    "<p>Hello from ESP32!</p>"
    "<p>Connected to ESP32 via Access Point (AP) Mode.</p>"
    "</body>"
    "</html>";

// HTTP GET handler for root "/"
static esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, html_page, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

// Start the HTTP server
static httpd_handle_t start_web_server(void)
{
    httpd_config_t config = HTTPD_DEFAULT_CONFIG();
    httpd_handle_t server = NULL;

    if (httpd_start(&server, &config) == ESP_OK) {
        ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
        httpd_uri_t uri_get = {
            .uri       = "/",
            .method    = HTTP_GET,
            .handler   = root_get_handler,
            .user_ctx  = NULL
        };
        httpd_register_uri_handler(server, &uri_get);
        return server;
    }
    ESP_LOGE(TAG, "Failed to start HTTP server");
    return NULL;
}

// Wi-Fi and IP event handler
static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) {
        ESP_LOGI(TAG, "Wi-Fi AP started! SSID: %s", MY_ESP_WIFI_SSID);

        esp_netif_ip_info_t ip_info;
        if (esp_netif_get_ip_info(ap_netif, &ip_info) == ESP_OK) {
            ESP_LOGI(TAG, "AP IP: " IPSTR, IP2STR(&ip_info.ip));
            ESP_LOGI(TAG, "Web server ready → http://" IPSTR "/", IP2STR(&ip_info.ip));
        }
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "Device connected: " MACSTR, MAC2STR(event->mac));
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "Device disconnected: " MACSTR, MAC2STR(event->mac));
    }
}

void app_main(void)
{
    ESP_LOGI(TAG, "Starting... Free heap: %d bytes", esp_get_free_heap_size());

    // Initialize NVS
    esp_err_t ret = nvs_flash_init();
    if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
        ESP_ERROR_CHECK(nvs_flash_erase());
        ret = nvs_flash_init();
    }
    ESP_ERROR_CHECK(ret);

    // Initialize TCP/IP stack and event loop
    ESP_ERROR_CHECK(esp_netif_init());
    ESP_ERROR_CHECK(esp_event_loop_create_default());

    // Create default Wi-Fi STA interface
    ap_netif = esp_netif_create_default_wifi_ap();

    // Initialize Wi-Fi
    wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
    ESP_ERROR_CHECK(esp_wifi_init(&cfg));

    // Register event handlers
    ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID,
                                                        &wifi_event_handler, NULL, NULL));

    // Configure Wi-Fi Access Point (AP) Configuration
    wifi_config_t wifi_config = {
        .ap = {
            .ssid = MY_ESP_WIFI_SSID,
            .ssid_len = strlen(MY_ESP_WIFI_SSID),
            .channel = 1,
            .password = MY_ESP_WIFI_PASS,
            .max_connection = 4,
            .authmode = WIFI_AUTH_WPA_WPA2_PSK,
        },
    };

    // Safety checks for correct SSID/Password to prevent reboots
    if (strlen(MY_ESP_WIFI_SSID) == 0 || strlen(MY_ESP_WIFI_SSID) > 32) {
        ESP_LOGE(TAG, "Invalid SSID length!");
        return;
    }
    if (strlen(MY_ESP_WIFI_PASS) == 0) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
        ESP_LOGW(TAG, "No password → Open AP (anyone can connect)");
    } else if (strlen(MY_ESP_WIFI_PASS) < 8) {
        wifi_config.ap.authmode = WIFI_AUTH_OPEN;
        wifi_config.ap.password[0] = '\0';
        ESP_LOGW(TAG, "Password too short (<8 chars) → Forced Open AP");
    } else {
        ESP_LOGI(TAG, "Using WPA2-PSK (password OK)");
    }

    // Start Wi-Fi interface in Access Point (AP) Mode
    ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
    ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
    ESP_ERROR_CHECK(esp_wifi_start());

    // Start web server in AP mode
    httpd_handle_t server = start_web_server();
    if (server) {
        ESP_LOGI(TAG, "Web server initialized. Connect to the Access Point (AP) now!");
    }
}

View raw code

How the Code Works

In this section, we’ll take a look at the code to see how it works.

Libraries

We start by including the required libraries:

  • stdio.h – the standard C library will be used for the printf function that prints the debugging information in the serial monitor;
  • string.h – the standard C library used for string manipulation;
  • FreeRTOS.h – provides the core FreeRTOS types and functions;
  • task.h – allows to use task management;
  • esp_system.h – system level functions like restart and hardware info;
  • esp_wifi.h – library for Wi-Fi configuration (station and access point modes and connection options);
  • esp_event.h – handles events like Wi-Fi status changes;
  • esp_log.h – offers a framework to format log messages in the serial monitor for debugging;
  • nvs_flash.h – stores key-value data in non-volatile storage (NVS) memory persistently ;
  • esp_netif.h – provides the network interface;
  • esp_http_server.h – library to create an HTTP server and handle HTTP requests/responses;
  • esp_mac.h – functions to print the MAC addresses with ESP_LOGI;
  • sdkconfig.h – includes the project’s configuration file.
#include <stdio.h>
#include <string.h>
#include "freertos/FreeRTOS.h"
#include "freertos/task.h"
#include "esp_system.h"
#include "esp_wifi.h"
#include "esp_event.h"
#include "esp_log.h"
#include "nvs_flash.h"
#include "esp_netif.h"
#include "esp_http_server.h"
#include "esp_mac.h"
#include "sdkconfig.h"

Access Point (AP) Wi-Fi Configuration

You need to define a SSID name and a password to access the ESP32. In this example we’re setting the ESP32 SSID name to ESP32_AP, but you can modify the name to whatever you want. The password is password123, but you can also modify it.

#define MY_ESP_WIFI_SSID      "ESP32_AP"
#define MY_ESP_WIFI_PASS     "password123"

Logging Tag

The “web_server_ap” prefix will be used to log all the debugging messages.

static const char *TAG = "web_server_ap";

HTML Web Page

This is the HTML web page that will be sent to the connected clients.

static const char *html_page = 
    "<!DOCTYPE html>"
    "<html>"
    "<head>"
    "<title>ESP-IDF: Web Server (Access Point)</title>"
    "<meta name=\"viewport\" content=\"width=device-width, initial-scale=1\">"
    "</head>"
    "<body>"
    "<h1>ESP-IDF: Web Server (Access Point)</h1>"
    "<p>Hello from ESP32!</p>"
    "<p>Connected to ESP32 via Access Point (AP) Mode.</p>"
    "</body>"
    "</html>";

HTTP GET Handler

This function (root_get_handler) runs every time a client (connected to the ESP32 access point) enters the ESP IP address in the browser (this makes a request to the root / web page). The httpd_resp_set_type function tells the browser that the file to be sent is in HTML format. Finally, the httpd_resp_send sends the html_page to the client.

static esp_err_t root_get_handler(httpd_req_t *req)
{
    httpd_resp_set_type(req, "text/html");
    httpd_resp_send(req, html_page, HTTPD_RESP_USE_STRLEN);
    return ESP_OK;
}

Start the HTTP Web Server

The start_web_server() function starts by configuring the server (uses the default port 80):

httpd_config_t config = HTTPD_DEFAULT_CONFIG();
httpd_handle_t server = NULL;

The httpd_start(&server, &config) starts the actual HTTP web server and starts listening to connections on port 80, so a client can connect to it. Then, we register the route, so when someone opens the / URL, it runs the root_get_handler() function that sends the html_page variable defined earlier.

if (httpd_start(&server, &config) == ESP_OK) {
    ESP_LOGI(TAG, "HTTP server started on port %d", config.server_port);
        
    // Register URI handler
    httpd_uri_t uri_get = {
        .uri       = "/",
        .method    = HTTP_GET,
        .handler   = root_get_handler,
        .user_ctx  = NULL
    };
    httpd_register_uri_handler(server, &uri_get);
        
    return server;
}

If it fails to start the HTTP web server, it prints a failed message.

ESP_LOGE(TAG, "Failed to start HTTP server");

Wi-Fi and IP event handler

This callback function runs when an event related to Wi-Fi happens.

static void wifi_event_handler(void* arg, esp_event_base_t event_base,
                               int32_t event_id, void* event_data)
{
    if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_START) {
        ESP_LOGI(TAG, "Wi-Fi AP started! SSID: %s", MY_ESP_WIFI_SSID);

        esp_netif_ip_info_t ip_info;
        if (esp_netif_get_ip_info(ap_netif, &ip_info) == ESP_OK) {
            ESP_LOGI(TAG, "AP IP: " IPSTR, IP2STR(&ip_info.ip));
            ESP_LOGI(TAG, "Web server ready → http://" IPSTR "/", IP2STR(&ip_info.ip));
        }
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STACONNECTED) {
        wifi_event_ap_staconnected_t* event = (wifi_event_ap_staconnected_t*) event_data;
        ESP_LOGI(TAG, "Device connected: " MACSTR, MAC2STR(event->mac));
    } else if (event_base == WIFI_EVENT && event_id == WIFI_EVENT_AP_STADISCONNECTED) {
        wifi_event_ap_stadisconnected_t* event = (wifi_event_ap_stadisconnected_t*) event_data;
        ESP_LOGI(TAG, "Device disconnected: " MACSTR, MAC2STR(event->mac));
    }
}

For example, when the ESP32 gets assigned an IP address, it triggers the IP_EVENT that prints this message in the Serial Monitor:

ESP_LOGI(TAG, "AP IP: " IPSTR, IP2STR(&ip_info.ip));

app_main(void)

When creating an ESP-IDF project, the app_main function will always be called to run. This function is where you need to write your code for any ESP-IDF applications; it is the equivalent of the setup() in Arduino programming. When the ESP32 boots, the ESP-IDF framework calls app_main.

void app_main(void)
{
    // your code goes here
}

In the app_main(void) function, you start by initializing the NVS (storage)—the ESP32 stores the Wi-Fi settings in flash.

esp_err_t ret = nvs_flash_init();
if (ret == ESP_ERR_NVS_NO_FREE_PAGES || ret == ESP_ERR_NVS_NEW_VERSION_FOUND) {
    ESP_ERROR_CHECK(nvs_flash_erase());
    ret = nvs_flash_init();
}
ESP_ERROR_CHECK(ret);

Then, initialize the TCP/IP stack required for network functions usage.

ESP_ERROR_CHECK(esp_netif_init());
ESP_ERROR_CHECK(esp_event_loop_create_default());

Start the Access Point

Start the Wi-Fi interface in Access Point (AP) mode (esp_netif_create_default_wifi_ap()), so other devices can establish a Wi-Fi connection with the ESP32 that acts as a router. It also initializes the Wi-Fi functionalities.

ap_netif = esp_netif_create_default_wifi_ap();

wifi_init_config_t cfg = WIFI_INIT_CONFIG_DEFAULT();
ESP_ERROR_CHECK(esp_wifi_init(&cfg));

Assign Wi-Fi events so that when the ESP32 establishes a Wi-Fi connection or obtains an IP address, the corresponding event runs.

ESP_ERROR_CHECK(esp_event_handler_instance_register(WIFI_EVENT, ESP_EVENT_ANY_ID, &wifi_event_handler, NULL, NULL));

These next few lines set the SSID and password and other Access Point configurations.

wifi_config_t wifi_config = {
    .ap = {
        .ssid = MY_ESP_WIFI_SSID,
        .ssid_len = strlen(MY_ESP_WIFI_SSID),
        .channel = 1,
        .password = MY_ESP_WIFI_PASS,
        .max_connection = 4,
        .authmode = WIFI_AUTH_WPA_WPA2_PSK,
    },
};

These next few lines check if the SSID and password are valid to launch the ESP32 in AP mode.

if (strlen(MY_ESP_WIFI_SSID) == 0 || strlen(MY_ESP_WIFI_SSID) > 32) {
    ESP_LOGE(TAG, "Invalid SSID length!");
    return;
}
if (strlen(MY_ESP_WIFI_PASS) == 0) {
    wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    ESP_LOGW(TAG, "No password → Open AP (anyone can connect)");
} else if (strlen(MY_ESP_WIFI_PASS) < 8) {
    wifi_config.ap.authmode = WIFI_AUTH_OPEN;
    wifi_config.ap.password[0] = '\0';
    ESP_LOGW(TAG, "Password too short (<8 chars) → Forced Open AP");
} else {
    ESP_LOGI(TAG, "Using WPA2-PSK (password OK)");
}

Set the ESP32 in AP mode with the configurations created earlier and start the Wi-Fi interface.

ESP_ERROR_CHECK(esp_wifi_set_mode(WIFI_MODE_AP));
ESP_ERROR_CHECK(esp_wifi_set_config(WIFI_IF_AP, &wifi_config));
ESP_ERROR_CHECK(esp_wifi_start());

Finally, start the web server:

httpd_handle_t server = start_web_server();
if (server) {
    ESP_LOGI(TAG, "Web Server initialized. Waiting for Wi-Fi connection...");
}

Build and Flash Code to the ESP32 Board

To build and flash ESP-IDF code to the ESP32, you always need to follow this procedure. You need to select the flash method (UART), the COM port number, the target device (ESP32), build the code, and finally, flash it to the board. All these commands are available in the bottom menu bar of VS Code.

Make sure all your options are correct (they may already be properly configured if you used the project wizard).

VS Code ESP-IDF Check All the Configured Settings UART COM Port Target Board

However, if your setup is not correct, follow the next instructions to ensure everything is set up correctly. First, click the “Star” icon and select the flash method as UART.

VS Code ESP-IDF Select Flash UART Option to Program Flash ESP32

While the ESP32 board is connected to your computer, click the COM Port (plug icon) and select the correct port number that refers to your ESP32.

VS Code ESP-IDF Programming ESP32 Board Select Correct COM Port Number

You also need to select the target device. Click on the chip icon at the bottom bar. In my case, I have an ESP32 with the esp32s3 chip.

VS Code Select the ESP32 S3 or Correct Target Device ESP-IDF

For this board, I also need to select the configuration: ESP32-S chip (via builtin USB-JTAG).

VS Code ESP-IDF Select the ESP32 S3 chip via built in USB JTAG Target Device

Finally, your command bar at the bottom of VS Code should have similar options selected.

VS Code ESP-IDF Check All the Configured Settings UART COM Port Target Board

Now, you can build the project by clicking the wrench icon (Build Project) as shown in the image below.

VS Code Build Project Example Code ESP32 ESP-IDF

The first time you build a project, it usually takes a bit more time. Once completed, it should print a similar message in the Terminal menu and show a “Build Successfully” message.

VS Code Build Example Project ESP32 ESP-IDF Success Message

This is the final step. You can now flash the ESP-IDF project to the ESP32 by clicking the “Flash Device” button (thunder icon).

VS Code Flash Hello World Code Project to ESP32 ESP-IDF

Depending on your board, you might need to hold down the on-board BOOT button on your ESP32 to put it into flashing mode. Once the process is completed, it will pop-up a info message saying “Flash Done“.

VS Code Flash Hello World Project to ESP32 ESP-IDF Done Success Message

Demonstration

If you followed all the steps, the example should be running successfully on your board. Open your Terminal window — click the “Monitor Device” tool that is illustrated with a screen icon.

VS Code Open Terminal Window Monitor Device ESP32 ESP-IDF

The ESP32 starts the Wi-Fi interface in Access Point mode and prints its IP address on the Serial Monitor. Copy that IP address (192.168.4.1) because you need it to access the ESP32 web server.

ESP-IDF Access Point AP Mode Serial Monitor Information

Note: if nothing shows up on the Serial Monitor, press the ESP32 “EN” button (ENABLE/RESET button next to the microUSB port).

To access the ESP32 web server, you must connect your device (computer or laptop) directly to your ESP32 Access Point:

  • Open the Wi-Fi menu
  • Select the ESP32_AP network
  • Enter the password (by default: password123)
  • Press the Next button to connect
ESP-IDF Connecting to ESP32 Access Point AP Mode

Finally, open your browser and type the ESP32 IP address (it should be http://192.168.4.1 ). You should see a similar web page being served by your ESP32.

ESP-IDF ESP32 Accessing Web Server Access Point AP Mode Demonstration

The Serial Monitor prints information related to the connected device.

ESP-IDF Access Point AP Mode Serial Monitor Client Connected

Wrapping Up

In this tutorial, you learned how to program the ESP32 with the ESP-IDF framework using VS Code to run an Access Point that hosts a simple web page. When the ESP32 is set as an access point, you don’t need a router to remotely connect to the ESP32. You can directly connect to its own Wi-Fi network.

You might like reading other ESP-IDF guides:

To learn more about creating ESP32 web servers (using Arduino Core), we recommend exploring our eBook: Build Web Servers with ESP32 and ESP8266 (3rd Edition).

Thanks for reading.



Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »
Learn how to build a home automation system and we’ll cover the following main subjects: Node-RED, Node-RED Dashboard, Raspberry Pi, ESP32, ESP8266, MQTT, and InfluxDB database DOWNLOAD »

Enjoyed this project? Stay updated by subscribing our newsletter!

Leave a Comment

Download Our Free eBooks and Resources

Get instant access to our FREE eBooks, Resources, and Exclusive Electronics Projects by entering your email address below.