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:
curl -o- https://raw.githubusercontent.com/nvm-sh/nvm/v0.39.7/install.sh | bashsource ~/.bashrcnvm 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.
| Method | Pros | Cons | Verdict |
|---|---|---|---|
| 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. |
/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).
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 Pin | BCM GPIO | Relay Module Pin | Function |
|---|---|---|---|
| Pin 11 | GPIO 17 | IN (Signal) | Control signal (3.3V logic) |
| Pin 2 | 5V Power | VCC | Powers the relay coil and optocoupler |
| Pin 6 | GND | GND | Common 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:
- 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=100toCONF_SWAPSIZE=1024 - Apply:
sudo dphys-swapfile setupthensudo dphys-swapfile swapon
- Run:
- 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:
- Missing User Permissions (Most Likely): Your user isn't in the
gpiogroup. Fix:sudo usermod -aG gpio $USER, then log out and log back in. - Pin is Locked by Another Process: A previous crash didn't unexport the pin, or
pigpiodis running in the background. Fix:sudo killall pigpiodor reboot the Pi.
- Run
free -hto verify your Swap row shows at least 1.0G. - Run
groupsto ensure your current user listsgpio,i2c, andspi. - Run
sudo lsof | grep gpioto 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.






