ESP32 Web Server using SPIFFS (SPI Flash File System)

In this tutorial we’ll show you how to build a web server that serves HTML and CSS files stored on the ESP32 filesystem. Instead of having to write the HTML and CSS text into the Arduino sketch, we’ll create separated HTML and CSS files.

For demonstration purposes, the web server we’ll build controls an ESP32 output, but it can be easily adapted for other purposes like displaying sensor readings.

Recommended reading: ESP8266 Web Server using SPIFFS

ESP32 Filesystem Uploader Plugin

To follow this tutorial you should have the ESP32 Filesystem Uploader plugin installed in your Arduino IDE. If you haven’t, follow the next tutorial to install it first:

Note: make sure you have the latest Arduino IDE installed, as well as the ESP32 add-on for the Arduino IDE. If you don’t, follow one of the next tutorials to install it:

Project Overview

Before going straight to the project, it’s important to outline what our web server will do, so that it is easier to understand.

  • The web server you’ll build controls an LED connected to the ESP32 GPIO 2. This is the ESP32 on-board LED. You can control any other GPIO;
  • The web server page shows two buttons: ON and OFF – to turn GPIO 2 on and off;
  • The web server page also shows the current GPIO state.

The following figure shows a simplified diagram to demonstrate how everything works.

  • The ESP32 runs a web server code based on the ESPAsyncWebServer library;
  • The HTML and CSS files are stored on the ESP32 SPIFFS (Serial Peripheral Interface Flash File System);
  • When you make a request on a specific URL using your browser, the ESP32 responds with the requested files;
  • When you click the ON button, you are redirected to the root URL followed by /on and the LED is turned on;
  • When you click the OFF button, you are redirected to the root URL followed by /off and the LED is turned off;
  • On the web page, there is a placeholder for the GPIO state. The placeholder for the GPIO state is written directly in the HTML file between % signs, for example %STATE%.

Installing Libraries

In most of our projects we’ve created the HTML and CSS files for the web server as a String directly on the Arduino sketch. With SPIFFS, you can write the HTML and CSS in separated files and save them on the ESP32 filesystem.

One of the easiest ways to build a web server using files from the filesystem is by using the ESPAsyncWebServer library. The ESPAsyncWebServer library is well documented on its GitHub page. For more information about that library, check the following link:

Installing the ESPAsyncWebServer library

Follow the next steps to install the ESPAsyncWebServer library:

  1. Click here to download the ESPAsyncWebServer library. You should have a .zip folder in your Downloads folder
  2. Unzip the .zip folder and you should get ESPAsyncWebServer-master folder
  3. Rename your folder from ESPAsyncWebServer-master to ESPAsyncWebServer
  4. Move the ESPAsyncWebServer folder to your Arduino IDE installation libraries folder

Installing the Async TCP Library for ESP32

The ESPAsyncWebServer library requires the AsyncTCP library to work. Follow the next steps to install that library:

  1. Click here to download the AsyncTCP library. You should have a .zip folder in your Downloads folder
  2. Unzip the .zip folder and you should get AsyncTCP-master folder
  3. Rename your folder from AsyncTCP-master to AsyncTCP
  4. Move the AsyncTCPfolder to your Arduino IDE installation libraries folder
  5. Finally, re-open your Arduino IDE

Organizing your Files

To build the web server you need three different files. The Arduino sketch, the HTML file and the CSS file. The HTML and CSS files should be saved inside a folder called data inside the Arduino sketch folder, as shown below:

Creating the HTML File

The HTML for this project is very simple. We just need to create a heading for the web page, a paragraph to display the GPIO state and two buttons.

Create an index.html file with the following content or download all the project files here:

<!DOCTYPE html>
<html>
<head>
  <title>ESP32 Web Server</title>
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <link rel="icon" href="data:,">
  <link rel="stylesheet" type="text/css" href="style.css">
</head>
<body>
  <h1>ESP32 Web Server</h1>
  <p>GPIO state: <strong> %STATE%</strong></p>
  <p><a href="/on"><button class="button">ON</button></a></p>
  <p><a href="/off"><button class="button button2">OFF</button></a></p>
</body>
</html>

View raw code

Because we’re using CSS and HTML in different files, we need to reference the CSS file on the HTML text. The following line should be added between the <head> </head> tags:

<link rel="stylesheet" type="text/css" href="style.css">

The <link> tag tells the HTML file that you’re using an external style sheet to format how the page looks. The rel attribute specifies the nature of the external file, in this case that it is a stylesheet—the CSS file—that will be used to alter the appearance of the page.

The type attribute is set to “text/css” to indicate that you’re using a CSS file for the styles. The href attribute indicates the file location; since both the CSS and HTML files will be in the same folder, you just need to reference the filename: style.css.

In the following line, we write the first heading of our web page. In this case we have “ESP32 Web Server”. You can change the heading to any text you want:

<h1>ESP32 Web Server</h1>

Then, we add a paragraph with the text “GPIO state: ” followed by the GPIO state. Because the GPIO state changes accordingly to the state of the GPIO, we can add a placeholder that will then be replaced for whatever value we set on the Arduino sketch.

To add placeholder we use % signs. To create a placeholder for the state, we can use %STATE%, for example.

<p>GPIO state: <strong>%STATE%</strong></p>

Attributing a value to the STATE placeholder is done in the Arduino sketch.

Then, we create an ON and an OFF buttons. When you click the on button, we redirect the web page to to root followed by /on url. When you click the off button you are redirected to the /off url.

<p><a href="/on"><button class="button">ON</button></a></p>
<p><a href="/off"><button class="button button2">OFF</button></a></p>

Creating the CSS file

Create the style.css file with the following content or download all the project files here:

html {
  font-family: Helvetica;
  display: inline-block;
  margin: 0px auto;
  text-align: center;
}
h1{
  color: #0F3376;
  padding: 2vh;
}
p{
  font-size: 1.5rem;
}
.button {
  display: inline-block;
  background-color: #008CBA;
  border: none;
  border-radius: 4px;
  color: white;
  padding: 16px 40px;
  text-decoration: none;
  font-size: 30px;
  margin: 2px;
  cursor: pointer;
}
.button2 {
  background-color: #f44336;
}

View raw code

This is just a basic CSS file to set the font size, style and color of the buttons and align the page. We won’t explain how CSS works. A good place to learn about CSS is the W3Schools website.

Arduino Sketch

Copy the following code to the Arduino IDE or download all the project files here. Then, you need to type your network credentials (SSID and password) to make it work.

/*********
  Rui Santos
  Complete project details at https://randomnerdtutorials.com  
*********/

// Import required libraries
#include "WiFi.h"
#include "ESPAsyncWebServer.h"
#include "SPIFFS.h"

// Replace with your network credentials
const char* ssid = "REPLACE_WITH_YOUR_SSID";
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

// Set LED GPIO
const int ledPin = 2;
// Stores LED state
String ledState;

// Create AsyncWebServer object on port 80
AsyncWebServer server(80);

// Replaces placeholder with LED state value
String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if(digitalRead(ledPin)){
      ledState = "ON";
    }
    else{
      ledState = "OFF";
    }
    Serial.print(ledState);
    return ledState;
  }
  return String();
}
 
void setup(){
  // Serial port for debugging purposes
  Serial.begin(115200);
  pinMode(ledPin, OUTPUT);

  // Initialize SPIFFS
  if(!SPIFFS.begin(true)){
    Serial.println("An Error has occurred while mounting SPIFFS");
    return;
  }

  // Connect to Wi-Fi
  WiFi.begin(ssid, password);
  while (WiFi.status() != WL_CONNECTED) {
    delay(1000);
    Serial.println("Connecting to WiFi..");
  }

  // Print ESP32 Local IP Address
  Serial.println(WiFi.localIP());

  // Route for root / web page
  server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  
  // Route to load style.css file
  server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
    request->send(SPIFFS, "/style.css", "text/css");
  });

  // Route to set GPIO to HIGH
  server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){
    digitalWrite(ledPin, HIGH);    
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });
  
  // Route to set GPIO to LOW
  server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){
    digitalWrite(ledPin, LOW);    
    request->send(SPIFFS, "/index.html", String(), false, processor);
  });

  // Start server
  server.begin();
}
 
void loop(){
  
}

View raw code

How the Code Works

First, include the necessary libraries:

#include "WiFi.h" 
#include "ESPAsyncWebServer.h" 
#include "SPIFFS.h"

You need to type your network credentials in the following variables:

const char* ssid = "REPLACE_WITH_YOUR_SSID"; 
const char* password = "REPLACE_WITH_YOUR_PASSWORD";

Next, create a variable that refers to GPIO 2 called ledPin, and a String variable to hold the led state: ledState.

const int ledPin = 2;
String ledState;

Create an AsynWebServer object called server that is listening on port 80.

AsyncWebServer server(80);

processor()

The processor() function is what will attribute a value to the placeholder we’ve created on the HTML file. It accepts as argument the placeholder and should return a String that will replace the placeholder. The processor() function should have the following structure:

String processor(const String& var){
  Serial.println(var);
  if(var == "STATE"){
    if(digitalRead(ledPin)){
      ledState = "ON";
    }
    else{
      ledState = "OFF";
    }
    Serial.print(ledState);
    return ledState;
  }
  return String();
}

This function first checks if the placeholder is the STATE we’ve created on the HTML file.

if(var == "STATE"){

If it is, then, accordingly to the LED state, we set the ledState variable to either ON or OFF.

if(digitalRead(ledPin)){
  ledState = "ON";
}
else{
  ledState = "OFF";
}

Finally, we return the ledState variable. This replaces the placeholder with the ledState string value.

return ledState;

setup()

In the setup(), start by initializing the Serial Monitor and setting the GPIO as an output.

Serial.begin(115200);
pinMode(ledPin, OUTPUT);

Initialize SPIFFS:

if(!SPIFFS.begin(true)){
  Serial.println("An Error has occurred while mounting SPIFFS");
  return;
}

Wi-Fi connection

Connect to Wi-Fi and print the ESP32 IP address:

WiFi.begin(ssid, password);
while (WiFi.status() != WL_CONNECTED) {
  delay(1000);
  Serial.println("Connecting to WiFi..");
}
Serial.println(WiFi.localIP());

Async Web Server

The ESPAsyncWebServer library allows us to configure the routes where the server will be listening for incoming HTTP requests and execute functions when a request is received on that route. For that, use the on() method on the server object as follows:

server.on("/", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(SPIFFS, "/index.html", String(), false, processor);
});

When the server receives a request on the root “/” URL, it will send the index.html file to the client. The last argument of the send() function is the processor, so that we are able to replace the placeholder for the value we want – in this case the ledState.

Because we’ve referenced the CSS file on the HTML file, the client will make a request for the CSS file. When that happens, the CSS file is sent to the client:

server.on("/style.css", HTTP_GET, [](AsyncWebServerRequest *request){
  request->send(SPIFFS, "/style.css","text/css");
});

Finally, you need to define what happens on the /on and /off routes. When a request is made on those routes, the LED is either turned on or off, and the ESP32 serves the HTML file.

server.on("/on", HTTP_GET, [](AsyncWebServerRequest *request){
  digitalWrite(ledPin, HIGH);
  request->send(SPIFFS, "/index.html", String(),false, processor);
});
server.on("/off", HTTP_GET, [](AsyncWebServerRequest *request){
  digitalWrite(ledPin, LOW);
  request->send(SPIFFS, "/index.html", String(),false, processor);
});

In the end, we use the begin() method on the server object, so that the server starts listening for incoming clients.

server.begin();

Because this is an asynchronous web server, you can define all the requests in the setup(). Then, you can add other code to the loop() while the server is listening for incoming clients.

Uploading Code and Files

Save the code as Async_ESP32_Web_Server or download all the project files here. Go to Sketch > Show Sketch Folder, and create a folder called data. Inside that folder you should save the HTML and CSS files.

Then, upload the code to your ESP32 board. Make sure you have the right board and COM port selected. Also, make sure you’ve added your networks credentials to the code.

After uploading the code, you need to upload the files. Go to Tools > ESP32 Data Sketch Upload and wait for the files to be uploaded.

When everything is successfully uploaded, open the Serial Monitor at a baud rate of 115200. Press the ESP32 “ENABLE” button, and it should print the ESP32 IP address.

Demonstration

Open your browser and type the ESP32 IP address. Press the ON and OFF buttons to control the ESP32 on-board LED. Also, check that the GPIO state is being updated correctly.

Wrapping Up

Using SPI Flash File System (SPIFFS) is specially useful to store HTML and CSS files to serve to a client – instead of having to write all the code inside the Arduino sketch.

The ESPAsyncWebServer library allows you to build a web server by running a specific function in response to a specific request. You can also add placeholders to the HTML file that can be replaced with variables – like sensor readings, or GPIO states, for example.

If you liked this project, you may also like:

This is an excerpt from our course: Learn ESP32 with Arduino IDE. If you like ESP32 and you want to learn more, we recommend enrolling in Learn ESP32 with Arduino IDE course.


Learn how to program and build projects with the ESP32 and ESP8266 using MicroPython firmware DOWNLOAD »

Learn how to program and build projects with the ESP32 and ESP8266 using MicroPython firmware DOWNLOAD »


Enjoyed this project? Stay updated by subscribing our weekly newsletter!

46 thoughts on “ESP32 Web Server using SPIFFS (SPI Flash File System)”

  1. Hi, incredible good… thanks!
    Can you store the CSS and HTML files into an SD card, so that you can actualize it every time you want without requiring to update your sketch?

    • You don’t need to re-compile whenever you want to upload the HTML files. They are stored in the ESP32 file system: SPIFFS. Totally separate memory from code – that was the point of this article and not to store the HTML in the code, but in the dedicated local filing system. Saves you needing any SD, its built into the ESP32 own memory map.

    • Hi Paul.
      Yes, alternatively you can store your files in an SD card.
      You need to use the SD.h library to manipulate the files on the SD card.
      Regards,
      Sara 🙂

  2. Hey Team, this looks awesome! Will have a go when I get back home.
    I’ll try the LED tutorial first, then implement some crazier stuff 😉
    (Probably a LED strip/matrix)

    Thanks!

  3. Well, I suppose SD cards can hold up to 32 G data, SPIFFS maybe 1 M
    Then SPIFFS is convenient with unfrequent changes (suppose html page countains data: if they are updated once a second -temperature; ca 10 chars, with a time stamp in a huamn friendly format -, it needs ca one day to fill SPIFFS …. and some decades to fill SD card).
    I hope this calculation is not absurd (and I am aware I missed number of repeated writes on SD cards, and maybe on ESP flash).

    • Hi Denis, you are right.
      As stated in this tutorial, using SPIFFS is useful to
      – Create configuration files with settings;
      – Save data permanently;
      – Create files to save small amounts of data instead of using a microSD card;
      – Save HTML and CSS files to build a web server;
      It shouldn’t be used to create datalogging projects that required large amounts of data or multiple updates for second.
      Thank you for your comment,
      Regards,
      Sara 🙂

  4. Hi guys, I have the webserver running with SPIFFS. I have tried adding a slider to get a value back into ESP without much success. Without using Ajax or Java, is there an easy way of getting a slider value fro the webpage into ESP32?

  5. Good job ! Is it possible to add image in the data folder for HTML file to reference it? Is it also possible to use bootstrap with the help of SPIFFS? Thanks a lot

    • Hi.
      Yes, you can use bootstrap.
      I think you can also add images in the data folder, but you need to be careful with the space it occupies (I haven’t tried images yet, but I think it might work).

  6. Hi Sara!
    I am having trouble uploading file in the Data folder with the following error message:

    SPIFFS not supported on esp32

  7. Thanks for an amazing tutorial, how to make led state update in all devices? and how many device can connect to this server ?i connected 4 device and all worked very good.only led state doesnt update in all devices. thanks

    • Hi Rashad.
      To update the LED state you need to refresh the device’s browser.
      If you want it to update automatically, you can use ajax in your webserver code, for example.
      You can connect as many clients as you want, as long as the connection closes after the response is sent.
      Regards,
      Sara 🙂

  8. I tried for several hours to install the ESP32 file loader as described. It does not show up under my tools drop down. Any suggestions? I am using Arduino 1.8.8 and win10

      • Thanks Sara 🙂 … I just did now the issue is I can’t upload the ESP32 Sketch data. I get the following error message:

        [SPIFFS] data : /home/object-undefined/Arduino/ESP32_Async_Web_Server/data
        [SPIFFS] start : 2691072
        [SPIFFS] size : 1468
        [SPIFFS] page : 256
        [SPIFFS] block : 4096
        /style.css
        /index.html
        [SPIFFS] upload : /tmp/arduino_build_905232/ESP32_Async_Web_Server.spiffs.bin
        [SPIFFS] address: 2691072
        [SPIFFS] port : /dev/ttyUSB0
        [SPIFFS] speed : 921600
        [SPIFFS] mode : dio
        [SPIFFS] freq : 80m

        Traceback (most recent call last):
        File “/home/object-undefined/Arduino/hardware/espressif/esp32/tools/esptool.py”, line 35, in
        import serial.tools.lists_ports as list_ports
        ImportError: No module named lists_ports
        SPIFFS Upload failed!

  9. I have done everything on you guide. but Serial monitor show
    Backtrace: 0x400874f8:0x3ffc64c0 0x400875f7:0x3ffc64e0 0x400d4793:0x3ffc6500 0x400eaf04:0x3ffc6530 0x400eb272:0x3ffc6550 0x400eb5a1:0x3ffc65a0 0x400ead18:0x3ffc6600 0x400eab5a:0x3ffc6650 0x400eabf3:0x3ffc6670 0x400eac3e:0x3ffc6690 0x400e9f44:0x3ffc66b0 0x400e9ef3:0x3ffc66d0 0x400d5b12:0x3ffc6700

    Rebooting…
    assertion “false && “item should have been present in cache”” failed: file “/Users/ficeto/Desktop/ESP32/ESP32/esp-idf-public/components/nvs_flash/src/nvs_item_hash_list.cpp”, line 85, function: void nvs::HashList::erase(size_t)
    abort() was called at PC 0x400d4793 on core 0

    How can I fix it?

    • Hi Nash.
      I’ve searched for a while and I found some people with the same problem but using other sketches.
      Unfortunately, I haven’t found a clear solution for that problem:
      github.com/espressif/arduino-esp32/issues/884
      github.com/espressif/arduino-esp32/issues/2159
      github.com/espressif/arduino-esp32/issues/1967

      In this discussion, they suggest erasing the flash memory before uploading the new sketch: bbs.esp32.com/viewtopic.php?t=8888

      I hope this helps in some way.

      Regards,
      Sara

  10. Hi,
    Do you have code for wifi controlled robot using ESP32 where we can control the direction of robot via web server?
    Thanks . Looking forward for your reply

  11. Hello Sara
    Tolling about ESP32 in AP mode From factory is programmed to accept maximum 4 clients connections Is known that can be extended to 8 or more
    Can you help me to increase the maximum connections to 8 at list

    Regards

  12. Hello Sara,

    i able to upload the sketch in my arduino ide, in the serial monitor showing the text just like above except the connecting line and ip address. it’s not showing at all. why is that?

    • Hi.
      That usually happens when people forget to insert their network credentials.
      Please make sure that you’ve entered your network credentials in the code and that they are correct.
      Also, make sure that your ESP32 is relatively close to your router.
      REgards,
      Sara

      • Hello Sara,

        i think found out the problem, it seems when i upload the sketch with tools->esp32 sketch data upload. it uploaded successfully, but when i run the serial monitor it kind of not work. it only read the previous uploaded sketch using the normal upload mode (ctrl-u)

  13. Hi Sara!
    I’ve already implemented a project using the WebServer.h library instead of the ESPAsyncWebServer library on a ESP32.

    Is there a way to handle SPIFFS with WebServer.h or I need to convert my project using the ESPAsyncWebServer library ?

  14. Hello ,
    The tutorial is very informative and explained with details. I’m curious to know whether we can upload the HTML file to SPIFFS using platform IO. is there any way to upload files in platform IO?

  15. Rebooting…
    assertion “false && “item should have been present in cache”” failed: file “/Users/ficeto/Desktop/ESP32/ESP32/esp-idf-public/components/nvs_flash/src/nvs_item_hash_list.cpp”, line 85, function: void nvs::HashList::erase(size_t)
    abort() was called at PC 0x400d4793 on core 0

    This error is caused when the Arduino IDE “Tools” options for ESP32 flashing MISBEHAVE. Not kidding! And it is possibly a result of a prior code that tweaked the nvs flash and SPIFFS partitions of ESP32.

    Solution:
    STEP 1:
    Restart Arduino IDE and check if you are getting the standard options on the “Tools” tab when flashing to ESP32, i.e. Flash Freq, Size, Mode, Partition Scheme etc.

    STEP 2:
    If these are present, go ahead and flash your code to ESP32. Make sure all options have been selected properly. SUCCESS! EXIT!

    STEP 3:
    If these (Flash Freq, Size, Mode, Partition Scheme etc. ) options do not appear under “Tools” tab, GO TO STEP 1, until STEP 2 becomes true.

    • Hi Ved.
      Thanks for sharing.
      I had a few readers reporting a similar issue and I didn’t know how to solve that.
      I hope this helps other people.
      Thank you 😀

  16. Could someone enlighten me on what would be involved in porting this sketch/files to a secure server (HTTPS).

    I can create the HTTPS server on my ESP32, connect to it but am at a loss in sending the html response from SPIFFS or in replacing the status as there doesn’t seem to be an equivalent processor function.

Leave a Reply to Adam Cancel reply

Download our Free eBooks and Resources

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