Raspberry Pi Node.js: Install, Express Server, and GPIO Guide

raspberry pi node js introduction

Raspberry Pi Node.js projects run JavaScript server-side: building HTTP APIs, reading sensors, controlling GPIO, and running real-time IoT applications. The correct install method on Bookworm is nvm (Node Version Manager) or the NodeSource repository; sudo apt install nodejs installs an outdated version. This guide covers the nvm install path, a working Express web server, GPIO control with the onoff package, and pm2 for process management and autostart on boot. For Pi IoT projects combining Node.js with MQTT and Node-RED, see Raspberry Pi IoT Projects: Sensors, MQTT, and Real Builds.

Last tested: Raspberry Pi OS Bookworm Lite 64-bit | May 2025 | Raspberry Pi 4 Model B (4GB) | Node.js 22 LTS (nvm 0.39), npm 10, Express 4.19, onoff 6.0, pm2 5.4

Key Takeaways

  • sudo apt install nodejs on Bookworm installs an outdated Node.js version from the Debian APT repository. For current Node.js LTS, use nvm or the NodeSource setup script. Always verify with node -v after install; if it returns 12.x or 14.x, the APT version was installed instead.
  • npm ships with Node.js on all current installs. Running npm install npm -g to “upgrade npm” installs the npm package into the nvm Node.js prefix and is the correct approach when a newer npm is needed. Separate npm installation via apt is not required and not correct for nvm-managed installs.
  • Use pm2 to run Node.js applications as persistent background processes on Raspberry Pi. pm2 handles process restart on crash, log management, and generates the systemd startup script so the application survives a reboot. It is the standard production process manager for Node.js on Linux.

Installing Node.js on Raspberry Pi with nvm

nvm (Node Version Manager) installs Node.js into the user’s home directory without requiring sudo for npm package installs. It supports switching between Node.js versions and is the most flexible install method for development. Install nvm:

curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash

After the script completes, close and reopen the terminal (or run source ~/.bashrc) to activate nvm. Install the current LTS release:

nvm install --lts
nvm use --lts
node -v
npm -v

Expected result: node -v prints the current LTS version (22.x as of mid-2025). npm -v prints the bundled npm version. If nvm: command not found appears, source the profile manually: source ~/.nvm/nvm.sh and add that line to ~/.bashrc if it is missing.

For a system-wide install that all users and systemd services can access without activating nvm, use the NodeSource setup script instead:

curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash -
sudo apt install -y nodejs

Replace 22.x with the current LTS major version. Check nodejs.org/en/about/previous-releases for current LTS status. The NodeSource method installs Node.js as a system package accessible to all users and systemd services, which is simpler for server deployments than nvm.

Raspberry Pi Node.js project flow: install with nvm, Express web server, GPIO with onoff, pm2 process management, and deployment

Building an Express Web Server on Raspberry Pi Node.js

Express is the standard Node.js web framework for building HTTP APIs and web servers. Create a project directory and initialise it:

mkdir ~/pi-server && cd ~/pi-server
npm init -y
npm install express

Create server.js:

const express = require('express');
const os = require('os');
const app = express();
const PORT = process.env.PORT || 3000;

app.use(express.json());

// Root route
app.get('/', (req, res) => {
  res.json({
    message: 'Raspberry Pi Node.js server',
    hostname: os.hostname(),
    platform: os.platform(),
    uptime_s: Math.floor(os.uptime())
  });
});

// Health check endpoint
app.get('/health', (req, res) => {
  res.json({ status: 'ok', timestamp: new Date().toISOString() });
});

app.listen(PORT, () => {
  console.log(`Server running on http://0.0.0.0:${PORT}`);
});

Start the server:

node server.js

Expected result: The terminal prints “Server running on http://0.0.0.0:3000”. From another device on the same network, navigate to http://[pi-hostname]:3000 or test from the Pi itself with curl http://localhost:3000. The JSON response includes the Pi’s hostname and uptime. If the port is refused, check UFW: sudo ufw allow 3000.

For a more complete API, add routes for sensor data. Reading CPU temperature from the Pi’s thermal zone:

const fs = require('fs');

app.get('/temperature', (req, res) => {
  try {
    const raw = fs.readFileSync(
      '/sys/class/thermal/thermal_zone0/temp', 'utf8'
    );
    const temp_c = parseInt(raw.trim()) / 1000;
    res.json({ cpu_temp_c: temp_c.toFixed(1) });
  } catch (err) {
    res.status(500).json({ error: 'Could not read temperature' });
  }
});

Controlling GPIO with Node.js on Raspberry Pi

The onoff npm package controls GPIO pins on Raspberry Pi from Node.js. It is the standard GPIO library for Node.js on Bookworm and wraps the Linux sysfs GPIO interface. Install it in the project directory:

npm install onoff

LED blink example. Connect an LED to GPIO17 with a 330-ohm resistor to GND:

const { Gpio } = require('onoff');

const led = new Gpio(17, 'out');

let state = 0;
const blink = setInterval(() => {
  state = state === 0 ? 1 : 0;
  led.writeSync(state);
  console.log(`LED: ${state ? 'ON' : 'OFF'}`);
}, 500);

// Clean up after 5 seconds
setTimeout(() => {
  clearInterval(blink);
  led.writeSync(0);
  led.unexport();
  console.log('Done.');
}, 5000);

Expected result: The LED blinks at 1Hz for 5 seconds then goes off. The terminal prints ON/OFF each half second. If Error: EACCES appears, the user lacks GPIO access. Add the user to the gpio group: sudo usermod -aG gpio $USER and log out and back in.

Button input and Express integration. Reading a button on GPIO4 and exposing its state via the Express API:

const { Gpio } = require('onoff');
const button = new Gpio(4, 'in', 'both', { debounceTimeout: 50 });

let buttonState = 0;

button.watch((err, value) => {
  if (err) throw err;
  buttonState = value;
  console.log(`Button: ${value ? 'pressed' : 'released'}`);
});

// In Express server:
app.get('/button', (req, res) => {
  res.json({ pressed: buttonState === 1 });
});

// Clean up on exit
process.on('SIGINT', () => {
  button.unexport();
  process.exit();
});

Expected result: GET /button returns {"pressed": false} at rest. Pressing the button (GPIO4 connected to 3.3V via a 10k pull-down, or configured as internal pull-up) changes the response to {"pressed": true}. The terminal logs each state change. Always call .unexport() on process exit to release the GPIO resource.

Managing Raspberry Pi Node.js Apps with pm2

pm2 keeps Node.js applications running as background processes, restarts them on crash, collects logs, and generates systemd startup scripts. Install it globally:

npm install -g pm2

Start the Express server under pm2:

pm2 start server.js --name pi-server
pm2 status

Configure pm2 to start automatically after a reboot:

pm2 startup

pm2 prints a sudo env PATH=... command. Copy and run that exact command. Then save the current process list:

pm2 save

Expected result: After running pm2 startup and pm2 save, reboot the Pi and verify the server starts automatically with pm2 status. The process should show online with uptime counting from boot. View logs with pm2 logs pi-server. If the process shows errored, check the logs for the startup error before saving.

For serving the Express API behind Nginx as a reverse proxy on port 80, configure Nginx to forward requests to the Node.js port. This is the standard production pattern for a Pi acting as a local web server or API host. For a full Nginx/Caddy reverse proxy setup, see Caddy Reverse Proxy Raspberry Pi: Complete Setup Guide.

For Node-RED, which provides a visual flow-based programming environment for Raspberry Pi IoT projects and pairs naturally with Node.js, see Node-RED Raspberry Pi: Complete Flow Automation and Dashboard Setup Guide.

FAQ

How do I install the latest Node.js on Raspberry Pi Bookworm?

Use nvm or the NodeSource setup script. Running sudo apt install nodejs installs the Debian repository version, which is outdated (often Node.js 18.x or older on Bookworm). With nvm: install nvm via the curl script, run nvm install --lts, and verify with node -v. For a system-wide install available to all users and systemd services, use curl -fsSL https://deb.nodesource.com/setup_22.x | sudo -E bash - followed by sudo apt install -y nodejs.

Which npm package controls GPIO in Node.js on Raspberry Pi?

The onoff package is the standard Node.js GPIO library for Raspberry Pi on Bookworm. Install it with npm install onoff. It exposes a Gpio class for reading and writing GPIO pins and supports interrupt-driven input via the watch() method. If access is denied, add the user to the gpio group with sudo usermod -aG gpio $USER. Always call .unexport() on process exit to release GPIO resources cleanly.

How do I run a Node.js app automatically on Raspberry Pi boot?

Use pm2. Install globally with npm install -g pm2. Start the app with pm2 start app.js. Run pm2 startup and execute the printed sudo command. Then run pm2 save to persist the process list. After a reboot, pm2 restarts all saved processes automatically via a systemd service. Use pm2 status to confirm processes are running and pm2 logs to view output.

Can Raspberry Pi Node.js handle real-time sensor data?

Yes. Node.js’s non-blocking event loop is well-suited to reading multiple sensors, writing to a database, and serving HTTP requests simultaneously without threading complexity. Use setInterval() for polling-based sensors and onoff‘s watch() for interrupt-driven GPIO events. For streaming sensor data to a browser in real time, use Server-Sent Events (SSE) with Express or a WebSocket library like ws. For MQTT-based sensor pipelines, the mqtt npm package connects to a Mosquitto broker on the same Pi.

Should I use Node.js or Python for Raspberry Pi GPIO projects?

Both work. Python has the advantage of gpiozero, which provides higher-level hardware abstractions than the onoff Node.js package, and a larger Raspberry Pi-specific library ecosystem. Node.js has the advantage of the npm ecosystem for HTTP, WebSocket, MQTT, and database clients, and is the natural choice if the project already has a JavaScript codebase or if the primary deliverable is a web API. For projects that are primarily hardware control with minimal networking, Python and gpiozero are the more straightforward path. For projects where a web server or real-time API is the main component, Node.js is a strong choice.

References:


About the Author

Chuck Wilson has been programming and building with computers since the Tandy 1000 era. His professional background includes CAD drafting, manufacturing line programming, and custom computer design. He runs PidiyLab in retirement, documenting Raspberry Pi and homelab projects that he actually deploys and maintains on real hardware. Every article on this site reflects hands-on testing on specific hardware and OS versions, not theoretical walkthroughs.

Last tested hardware: Raspberry Pi 4 Model B (4GB). Last tested OS: Raspberry Pi OS Bookworm Lite 64-bit. Node.js 22 LTS via nvm, Express 4.19, onoff 6.0, pm2 5.4.