Learn how to create a web server with the Raspberry Pi Pico that serves files saved on its filesystem. Instead of writing the HTML directly in the MicroPython script, it can be more practical to have the HTML and other necessary files in a separate folder on the Raspberry Pi Pico filesystem.

This tutorial is only compatible with Raspberry Pi Pico W and Raspberry Pi Pico 2W that come with Wi-Fi support.
Table of Contents
- Raspberry Pi Pico – Serve Files from the Filesystem (HTML File)
- Raspberry Pi Pico Web Server (serve files from filesystem) – HTML, CSS, and JavaScript
We’ll modify the example in this previous tutorial to serve the HTML that is saved on the filesystem, instead of writing it directly in the script. We’ll also add a CSS and a JavaScript file, so you learn how to serve multiple files.
If this is your first time creating a web server with the Raspberry Pi Pico programmed with MicroPython, we suggest following these tutorials first to learn more about the process and all the fundamentals:
- Raspberry Pi Pico: Web Server (MicroPython)
- Raspberry Pi Pico W: Asynchronous Web Server (MicroPython)
Prerequisites – MicroPython Firmware
To follow this tutorial, you need MicroPython firmware installed on your Raspberry Pi Pico board. You also need an IDE to write and upload the code to your board.
The recommended MicroPython IDE for the Raspberry Pi Pico is Thonny IDE. Follow the next tutorial to learn how to install Thonny IDE, flash MicroPython firmware, and upload code to the board.
If you’re still getting started with the Raspberry Pi Pico, follow one of these getting-started guides:
- Getting Started with Raspberry Pi Pico 2 and Pico 2 W
- Getting Started with Raspberry Pi Pico (and Pico W)
Raspberry Pi Pico – Serve Files from the Filesystem (HTML File)
Before proceeding to the actual web server project we’ll cover in this tutorial, we’ll create a simple “Hello, World!” web server to show you the steps you need to follow to create a web server with the Pico W that serves the files saved in the filesystem.
This is useful if you want to create and serve separate HTML, CSS, JavaScript, or other files, instead of having to write everything in a variable in the MicroPython script.
Here are the steps you need to follow.
1) Open a new file in Thonny IDE and copy the following HTML text.
<!DOCTYPE html>
<html>
<head>
<title>Pico Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
</head>
<body>
<h1>Raspberry Pi Pico Web Server</h1>
<p>Hello, World!</p>
</body>
</html>

2) Make sure Thonny IDE is connected to your RPi Pico. After that, upload the file to the Pico’s filesystem. Go to File > Save as... and select Raspberry Pi Pico.

3) Save the file with the name index.html.

Now, the HTML file is saved in the Pico’s filesystem. If you go to View > Files, you should see that the index.html file is saved in the Pico’s filesystem.

4) Create another file in Thonny IDE. This one will be the MicroPython Script.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-web-server-filesystem-micropython/
# Import necessary modules
import network
import socket
import time
# Wi-Fi credentials
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
# Variable to save the HTML file path
HTML_FILE_PATH = "index.html"
# Function to read and return HTML content from the file
def read_file(filepath):
with open(filepath, "r") as file:
return file.read()
# Init Wi-Fi Interface
def init_wifi(ssid, password):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
# Connect to your network
wlan.connect(ssid, password)
# Wait for Wi-Fi connection
connection_timeout = 10
while connection_timeout > 0:
print(wlan.status())
if wlan.status() >= 3:
break
connection_timeout -= 1
print('Waiting for Wi-Fi connection...')
time.sleep(1)
# Check if connection is successful
if wlan.status() != 3:
print('Failed to connect to Wi-Fi')
return False
else:
print('Connection successful!')
network_info = wlan.ifconfig()
print('IP address:', network_info[0])
return True
if not init_wifi(ssid, password):
print("Exiting program.")
else:
try:
# Set up socket and start listening
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen()
print('Listening on', addr)
# Main loop to listen for connections
while True:
try:
conn, addr = s.accept()
print('Got a connection from', addr)
# Receive and parse the request
request = conn.recv(1024)
request_str = request.decode('utf-8')
print('Request content:')
# Generate HTML response
response = read_file(HTML_FILE_PATH)
# Send the HTTP response and close the connection
conn.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
conn.send(response)
conn.close()
except OSError as e:
conn.close()
print('Connection closed')
except KeyboardInterrupt:
print('Server stopped by user.')
In our script, we need to:
- define the HTML file path in the filesystem: in our case, it is in the root directory of the filesystem, so it’s simply index.html.
# Variable to save the HTML file path
HTML_FILE_PATH = "index.html
- read the file from the filesystem and return its content. We can create a function to do that as follows:
# Function to read and return HTML content from the file
def read_file(filepath):
with open(filepath, "r") as file:
return file.read()
To learn more about handling files with the Raspberry Pi Pico, you can check the following tutorial: Raspberry Pi Pico: Handling Files and Directories (MicroPython).
This is a simple web server that only returns the HTML page when you access the RPi IP address. So, the only request we need to handle is on the root (/) URL. We have a main loop that listens for HTTP requests.
# Main loop to listen for connections
while True:
try:
conn, addr = s.accept()
print('Got a connection from', addr)
# Receive and parse the request
request = conn.recv(1024)
request_str = request.decode('utf-8')
print('Request content:')
When we receive a request, we prepare our response. The content will be the HTML text saved in the index.html file. So, to generate the HTML response, we can simply read the content of our HTML file using the read_file() function we created earlier and pass as argument the HTML_FILE_PATH.
# Generate HTML response
response = read_file(HTML_FILE_PATH)
Finally, we can send the response. First, we prepare the HTTP header, specifying the content type: we’re sending HTML (Content-type: text/html).
conn.send('HTTP/1.0 200 OK\r\nContent-type: text/html\r\n\r\n')
Then, we can send the actual HTML text. It’s saved in the response variable.
conn.send(response)
Finally, close the connection with the server.
conn.close()
These are the steps you need to follow if you want to serve files that are saved in the Pico’s filesystem.
Testing the Web Server
Don’t forget to insert your network credentials in the following lines.
# Wi-Fi credentials
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
Now, to test the web server, upload the MicroPython script to the Raspberry Pi Pico and save it with the name main.py.

Or use the green run button to run the code on the board without uploading.

With a connection established between the Pico and Thonny IDE, the Pico’s IP address will be printed in the Serial Monitor.

Open a browser and type its IP address. It should serve the HTML file that you saved in the filesystem.

If you want to serve more files, like a CSS file or a JavaScript file, you’ll need to read the HTTP request and serve the response accordingly by getting the requested file in the filesystem and preparing the header with the appropriate Content-type. We’ll show you how to do that next.
Raspberry Pi Pico Web Server (serve files from filesystem)
Before proceeding, let’s just take a quick look at the features of the web server we’ll create:

Here’s what our example does:
- It is an asynchronous web server.
- Creates a web server that serves an HTML web page with:
- two buttons to control an LED on and off (the Pico’s built-in LED)
- a section to display the Pico internal temperature sensor (read with the picozero package)
- a button to request a new temperature reading
- the HTML web page is saved on the Pico’s filesystem
- It also serves a CSS file to style the web page (saved in the filesystem)
- And serves a JavaScript file (also saved in the filesystem) to handle what happens when we click on the buttons

This web server is based on a simpler one we’ve built in this previous tutorial. So, if you want to start with something more basic, check it out first: Raspberry Pi Pico W: Asynchronous Web Server (MicroPython).
Creating the HTML File
Open a new file in Thonny IDE and copy the following HTML text.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Pico Web Server</title>
<meta name="viewport" content="width=device-width, initial-scale=1">
<link rel="stylesheet" type="text/css" href="style.css">
<script src="script.js" defer></script>
</head>
<body>
<h1>Raspberry Pi Pico Web Server</h1>
<p>LED State: <strong id="led-status" class="status">{state}</strong></p>
<p>
<button onclick="turnOn()" class="button">ON</button>
<button onclick="turnOff()" class="button button2">OFF</button>
</p>
<p>
<span class="sensor-labels">Temperature:</span>
<span id="temp_c" class="temp-value">{temperature_c}</span><span class="units">°C</span>
/
<span id="temp_f" class="temp-value">{temperature_f}</span><span class="units">°F</span>
</p>
<p>
<button onclick="getNewReading()" class="button-refresh" style="width: 220px;">
Get New Reading
</button>
</p>
</body>
</html>
Save this file on the Raspberry Pi Pico filesystem. File > Save as… and select Raspberry Pi Pico. Name it index.html.
How does it work?
Let’s take a quick look at the parts of the HTML file that are relevant for this example. Alternatively, you can skip to the next section.
Include CSS and JavaScript Files
We need to add that we’ll include a CSS and a JavaScript file. When the HTML page loads, it will make a request to get those files: style.css and script.js.
<link rel="stylesheet" type="text/css" href="style.css">
<script src="script.js" defer></script>
Display LED State
We have a section to display the current LED state. It includes a placeholder that we can use to add the current LED state using an F-string in the MicroPython code {state}. The section to hold the LED state also has the led-status id (id=”led-status”), so that we can refer to it by its ID in the JavaScript file.
<p>LED State: <strong id="led-status" class="status">{state}</strong></p>

ON and OFF Buttons
Then, we have a paragraph with two buttons to control the LED. When you click on the ON or OFF buttons, the turnOn() and turnOff() JavaScript functions will be called. We’ll see in the next section how those functions work.
<p>
<button onclick="turnOn()" class="button">ON</button>
<button onclick="turnOff()" class="button button2">OFF</button>
</p>

Display Temperature Values
There’s a section to display the temperature values. Once again, we have an ID so that we can refer to that section of the HTML file in the JavaScript file (id=”temp_c” and id=”temp_f”) and we also have placeholders {temperature_c} and {temperature_f}).
<p>
<span class="sensor-labels">Temperature:</span>
<span id="temp_c" class="temp-value">{temperature_c}</span><span class="units">°C</span>
/
<span id="temp_f" class="temp-value">{temperature_f}</span><span class="units">°F</span>
</p>

Get New Temperature Reading
Finally, there’s a button to request a new temperature reading. When you click on that button, it will call the getNewReadings() JavaScript function.
<p>
<button onclick="getNewReading()" class="button-refresh" style="width: 220px;">
Get New Reading
</button>
</p>

Creating the JavaScript File
Open a new file in Thonny IDE and copy the following JavaScript code. Upload it to the Raspberry Pi Pico with the name script.js.
// Turn LED ON
async function turnOn() {
try {
const response = await fetch('/lighton');
const data = await response.json();
document.getElementById('led-status').textContent = data.state;
} catch (error) {
console.error('Error turning LED ON:', error);
alert('Failed to turn LED ON');
}
}
// Turn LED OFF
async function turnOff() {
try {
const response = await fetch('/lightoff');
const data = await response.json();
document.getElementById('led-status').textContent = data.state;
} catch (error) {
console.error('Error turning LED OFF:', error);
alert('Failed to turn LED OFF');
}
}
// Get new temperature reading
async function getNewReading() {
try {
const response = await fetch('/temperature');
const data = await response.json();
document.getElementById('temp_c').textContent = data.temperature_c;
document.getElementById('temp_f').textContent = data.temperature_f;
} catch (error) {
console.error('Error fetching temperature:', error);
alert('Failed to get new reading');
}
}
How Does it Work?
We’ll briefly explain this JavaScript code. Alternatively, you can skip to the next section.
Turn LED ON/OFF
The turnOn() function makes a request on the /lighton path. The response will be a JSON in the following format (this is handled in the MicroPython file).
{
"state": state
}
When it receives the response, it uses the response content, data.state, to update the LED state on the webpage.
async function turnOn() {
try {
const response = await fetch('/lighton');
const data = await response.json();
updateLedStatus(data.state);
} catch (error) {
console.error('Error turning LED ON:', error);
alert('Failed to turn LED ON');
}
}
The turnOff() function works in a similar way, but for the OFF button.
// Turn LED OFF
async function turnOff() {
try {
const response = await fetch('/lightoff');
const data = await response.json();
updateLedStatus(data.state);
} catch (error) {
console.error('Error turning LED OFF:', error);
alert('Failed to turn LED OFF');
}
}
Getting a New Temperature Reading
When you click the Get New Reading button, it calls the getNewReading() JavaScript function that will make a request on the /temperature URL. The request will be received on the server (RPi Pico), and in the MicroPython file we handle what happens when we receive that request. We send a JSON response in the following format:
{
"temperature_c": temperature_c,
"temperature_f": temperature_f
}
The getNewReading() function gets the data and places it in the corresponding HTML elements.
async function getNewReading() {
try {
const response = await fetch('/temperature');
const data = await response.json();
document.getElementById('temp_c').textContent = data.temperature_c;
document.getElementById('temp_f').textContent = data.temperature_f;
} catch (error) {
console.error('Error fetching temperature:', error);
alert('Failed to get new reading');
}
}
Creating the CSS File
Open a new file in Thonny IDE and copy the following CSS. Save it on the Raspberry Pi Pico with the name style.css.
html {
font-family: Arial;
background-color: #FFFFF7;
color: #333;
margin: 0;
padding: 0;
}
body {
max-width: 920px;
width: 100%;
margin: 0 auto;
text-align: center;
padding: 15px;
box-sizing: border-box;
}
h1 {
color: #0F3376;
padding: 25px 0 15px 0;
font-size: 2.1rem;
margin: 0;
}
p {
font-size: 1.55rem;
margin: 20px auto;
}
.button {
display: inline-block;
background-color: #00A676;
border: none;
border-radius: 6px;
color: white;
padding: 14px 32px;
text-decoration: none;
font-size: 1.25rem;
margin: 8px 12px;
cursor: pointer;
transition: background-color 0.3s ease, transform 0.1s ease;
box-shadow: 0 3px 8px rgba(0, 0, 0, 0.2);
width: 170px;
}
.button:hover {
background-color: #008c63;
transform: translateY(-2px);
}
.button2 {
background-color: #f44336;
}
.button2:hover {
background-color: #d32f2f;
}
.button-refresh {
background-color: #f0f0f0;
color: #444;
border: 1px solid #bbb;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
font-size: 1.15rem;
width: 230px;
padding: 12px 24px;
}
.button-refresh:hover {
background-color: #e0e0e0;
border-color: #999;
transform: translateY(-1px);
}
.sensor-labels {
font-size: 1.55rem;
font-weight: bold;
color: #444;
margin-right: 12px;
}
.temp-value {
font-size: 1.9rem;
font-weight: lighter;
color: #0F3376;
}
.units {
font-size: 1.45rem;
color: #555;
margin-left: 3px;
}
.status {
font-weight: bold;
color: #0F3376;
}
a {
text-decoration: none;
}
MicroPython Script: Web Server – Serve Files from the Filesystem
Now that you have all the files you want to serve on the Raspberry Pi Pico filesystem, let’s create the web server to serve those files, control the GPIO, and send the temperature when requested.
# Rui Santos & Sara Santos - Random Nerd Tutorials
# Complete project details at https://RandomNerdTutorials.com/raspberry-pi-pico-web-server-filesystem-micropython/
# Import necessary modules
import network
import socket
import time
from picozero import pico_temp_sensor #must install the picozero package
from machine import Pin
import json
ssid = 'REPLACE_WITH_YOUR_SSID'
password = 'REPLACE_WITH_YOUR_PASSWORD'
# Constant variables to save the files path
HTML_FILE_PATH = "index.html"
CSS_FILE_PATH = "style.css"
JS_FILE_PATH ="script.js"
# Create an LED object on pin 'LED' (this is the onboard LED)
led = Pin('LED', Pin.OUT)
# Initialize LED state
state = 'OFF'
# Function to read content from the file
def read_file(filepath):
with open(filepath, "r") as file:
return file.read()
# Get sensor readings
# Pico internal temperature sensor
def get_temperature():
temperature_c = pico_temp_sensor.temp
temperature_f = temperature_c * (9/5) + 32
temperature_c = round(temperature_c)
temperature_f = round(temperature_f)
return temperature_c, temperature_f
# HTML template for the webpage
def webpage(state):
html_content = read_file(HTML_FILE_PATH)
temperature_c, temperature_f = get_temperature()
html = html_content.format(state=state, temperature_c=temperature_c, temperature_f=temperature_f)
return html
# Init Wi-Fi Interface
def init_wifi(ssid, password):
wlan = network.WLAN(network.STA_IF)
wlan.active(True)
# Connect to your network
wlan.connect(ssid, password)
# Wait for Wi-Fi connection
connection_timeout = 10
while connection_timeout > 0:
print(wlan.status())
if wlan.status() >= 3:
break
connection_timeout -= 1
print('Waiting for Wi-Fi connection...')
time.sleep(1)
# Check if connection is successful
if wlan.status() != 3:
print('Failed to connect to Wi-Fi')
return False
else:
print('Connection successful!')
network_info = wlan.ifconfig()
print('IP address:', network_info[0])
return True
if not init_wifi(ssid, password):
print("Exiting program.")
else:
try:
# Set up socket and start listening
addr = socket.getaddrinfo('0.0.0.0', 80)[0][-1]
s = socket.socket()
s.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
s.bind(addr)
s.listen()
print('Listening on', addr)
# Main loop to listen for connections
while True:
try:
conn, addr = s.accept()
print('Got a connection from', addr)
# Receive and parse the request
request = conn.recv(1024)
request_str = request.decode('utf-8')
print('Request content:')
try:
path = request.split()[1]
print('Requested path:', path)
except IndexError:
pass
# Process the request and update variables19
if path == b'/lighton':
print('LED on')
led.value(1)
state = 'ON'
response = json.dumps({"state": state})
content_type = 'application/json'
elif path == b'/lightoff':
print('LED off')
led.value(0)
state = 'OFF'
response = json.dumps({"state": state})
content_type = 'application/json'
elif path == b'/temperature':
print('New temperature value requested')
temperature_c, temperature_f = get_temperature()
data = {
"temperature_c": temperature_c,
"temperature_f": temperature_f
}
response = json.dumps(data)
content_type = 'application/json'
# process the request and send the files
elif path == b'/style.css':
response = read_file(CSS_FILE_PATH)
content_type = 'text/css'
elif path == b'/script.js':
response = read_file(JS_FILE_PATH)
content_type = 'text/javascript'
else: # / root path
response = webpage(state)
content_type = 'text/html'
# Send the HTTP response and close the connection
conn.send(f'HTTP/1.0 200 OK\r\nContent-type: {content_type}\r\n\r\n')
conn.send(response)
conn.close()
except OSError as e:
conn.close()
print('Connection closed')
except KeyboardInterrupt:
print('Server stopped by user.')
Install the picozero package
In this example, we’ll send the Pico’s internal temperature sensor to the web page. To get the temperature, we’ll use the picozero package. In Thonny IDE, go to Tools > Manage packages.. search for picozero and install the picozero @PyPI package.
How Does it Work?
Let’s take a quick look at how the code works. Alternatively, you can skip to the Demonstration section.
File Path
In this example, we need to serve three different files: index.html, style.css, and script.js. We start by creating paths for those files. They are saved on the root directory of the filesystem, so we just need to refer to their names.
# Constant variables to save the files path
HTML_FILE_PATH = "webpage.html"
CSS_FILE_PATH = "style.css"
JS_FILE_PATH ="script.js"
Read the File from the Filesystem
Like in the previous basic example, we also need a function to read and return the content of the files saved in the filesystem: read_file().
# Function to read content from the file
def read_file(filepath):
with open(filepath, "r") as file:
return file.read()
HTML Template
The webpage() function returns the web page with the current GPIO state and the latest temperature readings. These values will be placed in the corresponding places in the content of the HTML file using F-strings. Check the placeholders in the HML file: {state}, {temperature_c} and {temperature_f}.
# HTML template for the webpage
def webpage(state):
html_content = read_file(HTML_FILE_PATH)
temperature_c, temperature_f = get_temperature()
html = html_content.format(state=state, temperature_c=temperature_c, temperature_f=temperature_f)
return html
We’ll serve the webpage using this template when you first access the root URL of the Raspberry Pi Pico. Then, when you click the buttons, we use JavaScript to only update the values without the need to refresh the whole page.
Handle Requests
Then, in our main loop (while True:), we check the content of the request and prepare and send the response accordingly.
# Main loop to listen for connections
while True:
try:
conn, addr = s.accept()
print('Got a connection from', addr)
# Receive and parse the request
request = conn.recv(1024)
request_str = request.decode('utf-8')
print('Request content:')
try:
path = request.split()[1]
print('Requested path:', path)
except IndexError:
pass
When you access the web page, it will request the style.css and script.js files. In that case, the client will make a request on the /style.css and /script.js paths. To prepare the response, we need to read the content of the CSS or Javascript files from the filesystem using the read_file() function. We also need to set the corresponding content type.
# process the request and send the files
elif path == b'/style.css':
response = read_file(CSS_FILE_PATH)
content_type = 'text/css'
elif path == b'/script.js':
response = read_file(JS_FILE_PATH)
content_type = 'text/javascript'
Finally, we send the response header with the content type and the response.
# Send the HTTP response and close the connection
conn.send(f'HTTP/1.0 200 OK\r\nContent-type: {content_type}\r\n\r\n')
conn.send(response)
We also send JSON variables to the client when he clicks on the buttons on the web page:
- ON button → request: /lighton
- OFF button → request: /lightoff
- Get New Reading button → request: /temperature
In this case, the content type for the response is application/json.
# Process the request and update variables19
if path == b'/lighton':
print('LED on')
led.value(1)
state = 'ON'
response = json.dumps({"state": state})
content_type = 'application/json'
elif path == b'/lightoff':
print('LED off')
led.value(0)
state = 'OFF'
response = json.dumps({"state": state})
content_type = 'application/json'
elif path == b'/temperature':
print('New temperature value requested')
temperature_c, temperature_f = get_temperature()
data = {
"temperature_c": temperature_c,
"temperature_f": temperature_f
}
response = json.dumps(data)
content_type = 'application/json'
Finally, if the request is different from all the previous ones, it means the client made a request on the root / URL or another URL not covered here. In that case, we simply send the HTML page with the current temperature and LED values. We prepare the response by calling the webpage() function and passing the current LED state as an argument.
else: # / root path
response = webpage(state)
content_type = 'text/html'
Testing the Web Server
Make sure you’ve uploaded the following files to the Pico filesystem:
- index.html
- javascript.js
- style.css
Make sure you installed the picozero package.
Then, upload or run this Micropython script to the RPi filesystem and save it as main.py or simply run it by clicking the green run button.
If you go to View > Files, you should get something as shown below.

The server should start, and the Pico IP address will be printed in the shell. Access the RPi Pico IP address on your web browser.
The index.html, style.css, and javascript.js will be served, and you’ll get access to the following webpage.

You can click the ON and OFF buttons to control the LED and the Get New Reading to request a new temperature value.
Wrapping Up
In this tutorial, you learned how to create a web server with the Raspberry Pi Pico that serves files saved on its filesystem. We first covered how to create a web server to serve a simple HTML page:
- We save the HTML file in the Pico’s filesystem.
- Then, we create a function to read that file and save its content in a MicroPython variable.
- Finally, we prepare an HTTP response with that content.
Later, we also covered how to send more files that you need for your webpage, like CSS and JavaScript files, and how to send JSON variables to be handled by the JavaScript.
This is a more practical way to create web servers with the Pico if you need multiple files. The files are organized, and it is easier to make changes later on.
We hope you’ve found this guide useful.
You may also like other RPi Pico tutorials:
- Raspberry Pi Pico: Web Server (Arduino IDE)
- Raspberry Pi Pico W: Save Network Credentials on a Separate File (MicroPython)
Learn more about the Raspberry Pi Pico with our resources:



