The Direct Answer: How to Install npm on Raspberry Pi

To install Node.js and npm on a Raspberry Pi without hitting EACCES permission errors or version locks, use Node Version Manager (NVM). This installs Node in your user directory, bypassing the need for sudo during global package installations.

Run these three commands in your Pi's terminal:

  1. curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bash
  2. source ~/.bashrc
  3. nvm install --lts

This pulls the latest Long Term Support (LTS) release (Node 20.x or 22.x in 2026) and automatically configures npm. You can verify the installation by running node -v and npm -v.

Decision Tree: NVM vs. NodeSource vs. Default apt

When setting up an embedded Node environment, you have three primary installation paths. Here is how they compare and which one you should pick.

MethodProsConsVerdict
Default apt
(sudo apt install npm)
Fastest to type; uses native Debian package manager. Raspberry Pi OS repos are notoriously outdated (often Node 12 or 14). Modern npm packages will fail to compile. Avoid. Will cause dependency hell.
NodeSource
(Setup script via curl)
Installs modern, specific versions directly into system directories. Requires sudo for all npm install -g commands; pollutes system paths; harder to downgrade. Use for Docker containers where user-space doesn't matter.
NVM
(Node Version Manager)
User-space install; no sudo needed for npm; easy version switching; isolates environments. Adds ~1 second to shell startup time; requires initial shell profile sourcing. DEFAULT PICK. Best for bare-metal Pi projects.
Pro Tip: If you are deploying a headless Pi that boots directly into a Node script via systemd, NVM works perfectly. Just ensure your systemd service file points to the NVM binary path: /home/pi/.nvm/versions/node/v20.x.x/bin/node.

Hardware Build: Node.js GPIO Relay Controller

To prove out the npm environment, we will build a web-controlled relay. This code targets the Raspberry Pi 4 Model B (4GB RAM).

Pi 5 Hardware Warning: The Raspberry Pi 5 uses the new RP1 southbridge chip, which completely changed the GPIO architecture. Legacy sysfs access (which the popular onoff npm library relies on) is broken on Pi 5. If you are using a Pi 5, you must use a libgpiod wrapper or the pigpio daemon. For maximum npm ecosystem compatibility in 2026, the Pi 4 remains the stable baseline for Node GPIO projects.

Parts List

  • Board: Raspberry Pi 4 Model B (4GB RAM variant - 1GB/2GB variants will hit memory limits during npm C++ compilation)
  • Power: Official 27W USB-C PD Power Supply (prevents brownout warnings under relay load)
  • Module: 5V 1-Channel Optocoupler Relay Module (active LOW)
  • Wiring: 3x Female-to-Female Dupont jumper wires

Pin Mapping Table

Raspberry Pi 4 PinBCM GPIORelay Module PinFunction
Pin 11GPIO 17IN (Signal)Control signal (3.3V logic)
Pin 25V PowerVCCPowers the relay coil and optocoupler
Pin 6GNDGNDCommon ground reference

The Code: Express Server with GPIO Error Handling

First, initialize your project and install the dependencies. The onoff library includes native C++ bindings (epoll), which is why having adequate RAM and swap space is critical.

mkdir pi-relay-server && cd pi-relay-server
npm init -y
npm install express onoff

Create a file named server.js and paste the following complete, compilable code. It includes explicit error handling for GPIO initialization and a cleanup routine to prevent pin-locking on exit.

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

const app = express();
const PORT = 3000;
const RELAY_PIN = 17;

let relay;

// Initialize GPIO with explicit error handling
try {
  // Pi 4 relay modules are often Active LOW (0 = ON, 1 = OFF)
  relay = new Gpio(RELAY_PIN, 'out', 'both', { activeLow: true });
  console.log(`GPIO ${RELAY_PIN} initialized successfully.`);
} catch (err) {
  console.error(`[FATAL] Failed to initialize GPIO ${RELAY_PIN}:`, err.message);
  console.error('Ensure you are running on a Pi 4, the pin is correct, and no other process holds it.');
  process.exit(1);
}

app.get('/relay/:state', (req, res) => {
  const state = req.params.state;
  
  if (state === 'on') {
    relay.writeSync(1);
    res.json({ status: 'success', message: 'Relay turned ON' });
  } else if (state === 'off') {
    relay.writeSync(0);
    res.json({ status: 'success', message: 'Relay turned OFF' });
  } else {
    res.status(400).json({ status: 'error', message: 'Use /relay/on or /relay/off' });
  }
});

app.get('/status', (req, res) => {
  res.json({ pin: RELAY_PIN, state: relay.readSync() === 1 ? 'ON' : 'OFF' });
});

// Graceful shutdown to unexport GPIO pins and prevent EBUSY errors on restart
process.on('SIGINT', () => {
  console.log('\nShutting down. Unexporting GPIO pins...');
  relay.writeSync(0); // Turn off relay before exiting
  relay.unexport();
  process.exit(0);
});

app.listen(PORT, () => {
  console.log(`Relay server running at http://localhost:${PORT}`);
  console.log(`Test: curl http://localhost:${PORT}/relay/on`);
});

Debugging: Exact Error Strings and Ranked Causes

Embedded Node.js environments fail in highly specific ways. If your build or script crashes, look for these exact error strings.

Error 1: npm ERR! code ENOMEM or Killed

Symptom: During npm install, the terminal hangs at the reify step, then abruptly prints Killed or throws an ENOMEM (Out of Memory) error. This happens because npm is trying to compile the epoll C++ addon and the Pi runs out of RAM.

Ranked Causes & Fixes:

  1. Insufficient Swap Space (90% of cases): Raspberry Pi OS defaults to 100MB of swap. Increase it to 1GB.
    • Run: sudo dphys-swapfile swapoff
    • Edit config: sudo nano /etc/dphys-swapfile
    • Change CONF_SWAPSIZE=100 to CONF_SWAPSIZE=1024
    • Apply: sudo dphys-swapfile setup then sudo dphys-swapfile swapon
  2. Using a Pi Zero / Pi 1 (10% of cases): Even with swap, the 512MB RAM and slow CPU will time out. Cross-compile on your main PC or upgrade to a Pi 4.

Error 2: Error: EPERM: operation not permitted, uv_loop_init

Symptom: The Node script crashes immediately upon executing new Gpio().

Ranked Causes & Fixes:

  1. Missing User Permissions (Most Likely): Your user isn't in the gpio group. Fix: sudo usermod -aG gpio $USER, then log out and log back in.
  2. Pin is Locked by Another Process: A previous crash didn't unexport the pin, or pigpiod is running in the background. Fix: sudo killall pigpiod or reboot the Pi.
The First 3 Things to Check When It Fails:
  1. Run free -h to verify your Swap row shows at least 1.0G.
  2. Run groups to ensure your current user lists gpio, i2c, and spi.
  3. Run sudo lsof | grep gpio to see if a zombie process is holding your BCM pin hostage.

Extending and Simplifying the Build

Once your baseline relay server is running, you need to decide how to manage it in a production or daily-use environment.

How to Extend: Daemonize with PM2

Running node server.js in an SSH session means the server dies when you close the terminal. Use PM2 to keep it alive across reboots.

npm install -g pm2
pm2 start server.js --name 'pi-relay'
pm2 save
pm2 startup

This generates a systemd service automatically. If the Pi loses power and reboots, PM2 will resurrect your Node server instantly.

How to Simplify: Pivot to Node-RED

If writing Express routes and handling SIGINT cleanup feels like overkill for a simple smart-home relay, drop the custom code entirely.

Install Node-RED via the official Pi script:

bash <(curl -sL https://raw.githubusercontent.com/node-red/linux-installers/master/deb/update-nodejs-and-nodered)

Node-RED provides a visual, browser-based flow editor. You can drag a 'HTTP In' node, wire it to a 'rpi gpio out' node, and deploy. It handles the underlying libgpiod or sysfs abstraction for you, making it the ultimate simplification for non-complex GPIO routing.

For deeper reading on managing Node releases and hardware configurations, refer to the Node.js releases documentation and the official Raspberry Pi hardware configuration guides.