Raspberry Pi: build a sensor REST API with Flask
Expose your Pi's sensors as a JSON REST API with Flask. GET readings, POST control commands, and the structure that keeps it honest.
Every Pi project with a sensor eventually needs to answer a question over the network: “what is the temperature right now?” A REST API is the answer that outlives every dashboard: any client (a phone, a cron job, another Pi, an ESP32) speaks HTTP JSON, no special app required.
Flask is the right tool at this scale: one file, no build step, and the whole API fits on one screen.
What you need
- Raspberry Pi with Raspberry Pi OS (any model; a Pi Zero 2 W handles this easily)
- SSH access or a terminal on the Pi
- A sensor already working (the DHT22 Python tutorial is a good sibling; this one uses a placeholder function you replace)
Install
sudo apt install python3-flask -y
# or, in a venv (the cleaner way):
python3 -m venv ~/venvs/sensorapi && ~/venvs/sensorapi/bin/pip install flask
The code
# ~/sensorapi/app.py
from flask import Flask, jsonify, request
import random # stand-in for your real sensor code
app = Flask(__name__)
# Stand-in for your sensor driver. Replace with gpiozero/DHT/etc.
def read_temp():
return round(20 + random.random() * 5, 1)
# The shared-state pattern: whatever the API can change, a background
# thread or task maintains. The API only reads/writes this dict.
state = {"pump": "off"}
@app.route("/api/reading", methods=["GET"])
def reading():
return jsonify({"temp": read_temp(), "unit": "C", "ok": True})
@app.route("/api/history", methods=["GET"])
def history():
n = request.args.get("n", default=24, type=int)
return jsonify({"points": get_history(min(n, 1000))})
@app.route("/api/pump", methods=["POST"])
def pump():
data = request.get_json(force=True)
if "on" not in data:
return jsonify(error="missing 'on' field"), 400
state["pump"] = bool(data["on"])
return jsonify({"pump": state["pump"]}), 200
if __name__ == "__main__":
app.run(host="0.0.0.0", port=8000)
Run it (python3 app.py), then from any machine on the LAN:
curl http://192.168.1.50:8000/api/reading
# {"ok": true, "reading": 22.3, "unit": "C"}
curl -X POST http://192.168.1.50:8000/api/pump -d '{"on": true}'
The response contract
JSON with a consistent shape, always. Three rules that age well:
- Status codes mean what they say. 200 read, 200 written, 400 bad input, 404 unknown route. Clients check codes, not prose.
- A boolean
okfield beats exceptions across the wire. Your future self parses JSON with one path, not two. - Version the path.
/api/readingtoday,/api/v2/readingwhen the shape changes. It costs nothing now and saves a migration later.
The real sensor wiring-in
Replace the stand-in with your driver. The one subtlety: GPIO access and Flask want to run forever, so either keep reads fast and inline:
# gpiozero read, 50 ms, fine inline
from gpiozero import DistanceSensor
sensor = DistanceSensor(echo=17, trigger=4)
@app.route("/api/reading")
def reading():
return jsonify({"distance_cm": round(sensor.distance * 100, 1)})
…or run slow sensors (e.g. an ultrasonic needing 60 ms settle) in a
background thread that updates state every second, and have every
route read state instantly. Threads started with threading.Thread
work fine alongside Flask for this.
Serving it for real
app.run() is the dev server. For anything always-on, run it under a
proper service:
# /etc/systemd/system/sensorapi.service
[Unit]
Description=Sensor API
After=network.target
[Service]
ExecStart=/home/brian/venvs/sensorapi/bin/python /home/brian/sensorapi/app.py
Restart=always
User=brian
[Install]
WantedBy=multi-user.target
sudo systemctl enable --now sensorapi
Now it survives reboots and crashes with restart. The port stays 8000 and the API lives at a stable address (e.g. pair this with the Wireguard tutorial to reach it from outside without opening ports).
What you learned
- Flask turns Pi sensor functions into HTTP endpoints in one file.
- The response contract (codes, shape, ok field) is what makes an API outlive its dashboard.
- systemd runs it as a service with restart-on-crash.
When something breaks
- Connection refused from another machine:
app.run()defaults to localhost only.host="0.0.0.0"is the fix you already have; check the Pi’s firewall if it still refuses. - 404s on the right route: trailing slash mismatch (e.g. Flask
treats
/api/reading/and/api/readingas different). Pick one and be consistent. - GPIO error on second run: a previous instance still holds the
pin.
sudo systemctl stop sensorapibefore re-running by hand. - API slow when history grows: you are reading a CSV on every GET. This is the SQLite tutorial’s whole reason to exist.
What to build next
- The SQLite logging tutorial gives /api/history a real database.
- The InfluxDB + Grafana tutorial is the “I want graphs, not endpoints” alternative.
- Pair with the Wireguard tutorial for secure remote access.