gwordal

Lesson 3 of 5 · 20 min

A web server and a JSON endpoint

Once the ESP32 has an IP address, anything on the network can ask it questions. The simplest way to answer is the same protocol your browser already speaks: HTTP. A phone, a dashboard or another program can request the temperature from a room by opening a URL. This lesson builds a small web server on the ESP32 with a JSON endpoint that reads a real sensor, and explains the HTTP you need to understand to debug it.

HTTP in one minute

HTTP is a text conversation. The client sends a request, the server sends one response, and that is the whole exchange. Here is what a browser sends when you open http://192.168.1.50/api/temp:

GET /api/temp HTTP/1.1
Host: 192.168.1.50
Accept: application/json

And the ESP32's reply:

HTTP/1.1 200 OK
Content-Type: application/json
Content-Length: 41

{"temp_c":23.4,"uptime_s":512,"rssi":-58}

The first line of a request holds the method and the path. The first line of a response holds the status code. The headers describe the body, and Content-Length says how many bytes follow. The ones you will meet most:

ItemCommon valuesMeaning
MethodGET, POSTGET reads and must not change anything, POST sends data to act on
Status 200OKThe request succeeded
Status 400Bad RequestThe client sent something invalid
Status 404Not FoundNo such path
Status 500Internal Server ErrorSomething broke on the device

A GET must be safe to repeat. A browser or a prefetching proxy may call it twice, so never switch a relay on with a GET. Use POST for actions.

Which server library

Two popular choices exist for the Arduino-ESP32 core:

  • WebServer is built in, needs no extra installation and handles one request at a time. You call server.handleClient() in loop().
  • ESPAsyncWebServer is a separate library that handles many connections in the background through callbacks, which suits pages with several simultaneous requests such as live dashboards.

We use the built-in WebServer here because the logic is easier to follow, and the endpoints you write look nearly the same in the async library. For a sensor that a few clients check every couple of seconds, one request at a time is plenty.

The sensor

Use a TMP36 temperature sensor. It runs from 3.3 V and outputs 10 mV per degree Celsius with a 500 mV offset, so that 0 degrees is 500 mV and negative temperatures are still positive voltages:

T = (V_mV - 500) / 10

At 750 mV that gives (750 - 500) / 10 = 25 °C. Connect the sensor output to GPIO 34, which is on ADC1, so it keeps working while Wi-Fi is on, as lesson 1 explained.

One ADC reading is noisy, with a few millivolts of jitter, and a few millivolts equals a few tenths of a degree. Averaging N independent samples reduces random noise by about sqrt(N), so 16 samples give a 4 times quieter result for a few tens of milliseconds of work.

The code

Reuse the connection code from lesson 2. This sketch focuses on the server, with a short blocking connect for clarity:

#include <WiFi.h>
#include <WebServer.h>
#include <Preferences.h>

WebServer server(80);              // HTTP default port
const uint8_t TEMP_PIN = 34;       // ADC1 channel, safe while Wi-Fi runs

// A tiny page that polls the JSON endpoint every 2 s
const char PAGE[] PROGMEM = R"HTML(
<!doctype html><meta name="viewport" content="width=device-width">
<title>ESP32 sensor</title>
<h1 id="t">...</h1>
<script>
async function tick() {
  const r = await fetch('/api/temp');
  const j = await r.json();
  document.getElementById('t').textContent = j.temp_c.toFixed(1) + ' C';
}
setInterval(tick, 2000); tick();
</script>
)HTML";

float readTempC() {
  uint32_t sum = 0;
  for (int i = 0; i < 16; i++) {                 // average 16 samples to cut noise
    sum += analogReadMilliVolts(TEMP_PIN);       // calibrated reading in mV
    delay(2);
  }
  float mV = sum / 16.0f;
  return (mV - 500.0f) / 10.0f;                  // TMP36: 10 mV per degree, 500 mV offset
}

void handleRoot() {
  server.send_P(200, "text/html", PAGE);
}

void handleTemp() {
  char body[96];
  snprintf(body, sizeof(body),
           "{\"temp_c\":%.1f,\"uptime_s\":%lu,\"rssi\":%d}",
           readTempC(), millis() / 1000, WiFi.RSSI());
  server.sendHeader("Access-Control-Allow-Origin", "*");  // let other web pages read it
  server.send(200, "application/json", body);
}

void handleNotFound() {
  server.send(404, "text/plain", "Not found");
}

void setup() {
  Serial.begin(115200);

  Preferences prefs;
  prefs.begin("wifi", true);
  String ssid = prefs.getString("ssid", "");
  String pass = prefs.getString("pass", "");
  prefs.end();

  WiFi.mode(WIFI_STA);
  WiFi.begin(ssid.c_str(), pass.c_str());
  while (WiFi.status() != WL_CONNECTED) delay(250);      // wait for the IP address
  Serial.println(WiFi.localIP());                        // open this in a browser

  server.on("/", HTTP_GET, handleRoot);                  // route table: path to handler
  server.on("/api/temp", HTTP_GET, handleTemp);
  server.onNotFound(handleNotFound);
  server.begin();
}

void loop() {
  server.handleClient();                                 // serve at most one pending request
}

Open the printed IP address in a browser and the page updates every two seconds. Open /api/temp directly to see the raw JSON, or call it from a terminal with curl http://192.168.1.50/api/temp.

Why JSON

The response is JSON because every language can parse it, including the browser's fetch, Python and other microcontrollers. Build it with snprintf into a fixed buffer: unlike string concatenation with String, it does not fragment the heap, which matters on a device that must run for months. Check the size too: this body is about 41 bytes, so a 96-byte buffer has room to spare.

Think about the cost of each request. A response is roughly 150 bytes of headers plus the body, so polling every 2 seconds moves about 190 bytes x 0.5 per second = 95 bytes per second, which is trivial. The real cost is time: each request keeps the server busy for about 35 ms, of which 32 ms is the sensor averaging (16 x 2 ms).

Check yourself

A TMP36 outputs 750 mV. Using T = (mV - 500) / 10, what is the temperature?

Check yourself

What status code should the server return when a client requests a path that does not exist?