Running Node.js for Raspberry Pi projects bridges the gap between high-level asynchronous JavaScript and low-level hardware control. While Python dominates the Pi ecosystem, Node.js offers a massive advantage for makers building networked IoT devices: non-blocking I/O. You can spin up an Express web server, listen for MQTT broker messages, and toggle GPIO pins all within the same event loop without managing complex threading.
However, controlling physical pins via JavaScript introduces specific hardware and OS-level pitfalls. This guide walks through a complete 12V DC fan control build, provides production-ready code, and debugs the exact error strings that trip up most developers—especially following the architectural shifts in recent Pi hardware.
Project Spec Sheet & Parts List
Estimated Time: 45 minutes
Target Board Variant: Raspberry Pi 4 Model B (4GB or 8GB RAM). Note: The code and sysfs debugging steps specifically target the Pi 4 due to the Pi 5's deprecation of the sysfs GPIO interface.
To demonstrate reliable hardware switching, we are bypassing basic LEDs and driving a 12V DC cooling fan using a logic-level N-Channel MOSFET. This teaches proper isolation and current handling, which is critical when moving from 3.3V logic to higher-voltage loads.
Required Components
| Component | Specification / Part Number | Estimated Cost |
|---|---|---|
| Microcontroller | Raspberry Pi 4 Model B (4GB) | $55.00 |
| Switching Component | IRLZ44N N-Channel Logic-Level MOSFET | $1.50 |
| Load | 12V DC PC Cooling Fan (4-pin or 2-pin) | $8.00 |
| Gate Resistor | 220Ω 1/4W Carbon Film | $0.10 |
| Pull-down Resistor | 10kΩ 1/4W Carbon Film | $0.10 |
| Flyback Diode | 1N4007 Rectifier Diode | $0.20 |
| Power Supply | 12V 2A DC Wall Adapter (for the fan) | $9.00 |
Wiring the Hardware: Pin Mapping & Connections
The Raspberry Pi's GPIO pins output 3.3V logic and can safely source only about 16mA per pin. The IRLZ44N MOSFET requires very little gate current to switch, but its internal gate capacitance can cause a brief inrush current when charging. The 220Ω gate resistor protects the Pi's GPIO pin from this spike.
Pin Mapping Table
| Raspberry Pi 4 Pin | BCM GPIO Number | Wiring Destination |
|---|---|---|
| Pin 12 | GPIO 18 (Hardware PWM0) | 220Ω Resistor → MOSFET Gate |
| Pin 14 | GND | MOSFET Source & 10kΩ Pull-down |
| Pin 17 | 3.3V Power | (Unused in this build, reserved) |
Circuit Path:
- Connect the 10kΩ pull-down resistor between the MOSFET Gate and Source. This ensures the fan stays off while the Pi is booting and GPIO 18 is in a high-impedance state.
- Connect the 12V fan's positive wire to the 12V power supply.
- Connect the fan's negative wire to the MOSFET Drain.
- Place the 1N4007 flyback diode across the fan terminals (cathode/stripe pointing toward the 12V positive side) to suppress inductive voltage spikes when the fan spins down.
Writing the Node.js GPIO Control Code
We will use the onoff library, which is the standard for Node.js GPIO manipulation on the Pi 4. It interfaces directly with the Linux sysfs GPIO subsystem.
Prerequisite: Ensure Node.js (v18 or v20 LTS) is installed on your Pi, then run npm install onoff in your project directory.
const { Gpio } = require('onoff');
// Define the hardware pin using BCM numbering
const FAN_PIN = 18;
const fan = new Gpio(FAN_PIN, 'out');
// Track state for graceful shutdown
let isRunning = false;
async function controlFan() {
try {
console.log(`Initializing GPIO ${FAN_PIN}...`);
// Turn fan ON
await fan.write(1);
isRunning = true;
console.log('Fan turned ON. Running for 5 seconds...');
// Wait for 5 seconds
await new Promise(resolve => setTimeout(resolve, 5000));
// Turn fan OFF
await fan.write(0);
isRunning = false;
console.log('Fan turned OFF.');
} catch (err) {
console.error('GPIO Operation Failed:', err.message);
console.error('Stack:', err.stack);
cleanupAndExit(1);
}
}
function cleanupAndExit(code = 0) {
console.log('Cleaning up GPIO resources...');
if (isRunning) {
try {
fan.writeSync(0); // Force low on exit
} catch (e) {
// Ignore sync write errors during cleanup
}
}
fan.unexport();
process.exit(code);
}
// Handle graceful shutdown on Ctrl+C or system kill signals
process.on('SIGINT', () => {
console.log('\nCaught SIGINT. Shutting down safely...');
cleanupAndExit(0);
});
process.on('SIGTERM', () => {
console.log('\nCaught SIGTERM. Shutting down safely...');
cleanupAndExit(0);
});
// Execute the main logic
controlFan();
Debugging: Exact Error Strings & Ranked Causes
When working with Node.js for Raspberry Pi hardware access, the OS layer will frequently block your script. If your code crashes, look for these exact error strings in your terminal.
Error 1: Error: EACCES: permission denied, open '/sys/class/gpio/export'
What it means: Your current Linux user does not have read/write permissions to the sysfs GPIO virtual filesystem.
Ranked Causes & Fixes:
- Running as standard user without udev rules: Fix this by adding your user to the
gpiogroup (sudo usermod -aG gpio $USER) and logging out/in, or set up a custom udev rule to grant group permissions to/sys/class/gpio/*. - AppArmor/SELinux blocking access: Rare on standard Raspberry Pi OS, but if you are running a hardened Ubuntu Server image, check
dmesgfor AppArmor denials.
Error 2: Error: ENOENT: no such file or directory, open '/sys/class/gpio/export'
What it means: The sysfs GPIO interface does not exist on your system. This is the Pi 5 Gotcha.
Ranked Causes & Fixes:
- You are using a Raspberry Pi 5: The Pi 5 uses the RP1 chip, and the Raspberry Pi Foundation officially deprecated the
sysfsinterface in favor of thegpiod(GPIO character device) API. Theonofflibrary will not work. Fix: Switch to the libgpiod bindings for Node.js, or downgrade to a Pi 4 for legacy sysfs projects. - sysfs is disabled in config: On newer Pi OS releases, sysfs might be disabled by default. Fix: Add
gpio=18=op,dhor re-enable the sysfs interface viasudo raspi-configunder Interfacing Options.
- Physical vs. BCM Pin Mismatch: Did you wire physical Pin 12 but define
const FAN_PIN = 12in the code? Theonofflibrary uses BCM numbering (which is 18 for physical pin 12). - Library Compatibility: Are you on a Pi 5? If yes,
onoffwill fail. Verify your board revision withcat /proc/device-tree/model. - Zombie GPIO Locks: Did a previous script crash without calling
unexport()? The OS might still think the pin is in use. Runecho 18 > /sys/class/gpio/unexportmanually to clear the lock.
Extending and Simplifying the Build
How to Simplify
If breadboard wiring and MOSFET gate capacitance calculations feel like overkill for your use case, simplify the hardware by using a pre-assembled Relay HAT. The Pimoroni Automation HAT or the Waveshare Relay HAT plug directly into the 40-pin header. They include onboard opto-isolators, flyback diodes, and transistor drivers. You simply map the I2C or direct GPIO pins in your Node.js script and toggle them without worrying about inductive kickback destroying your Pi.
How to Extend
To turn this local script into a networked IoT node, wrap the GPIO logic in an MQTT subscriber. By installing the mqtt npm package, your Pi can listen to a Home Assistant broker topic (e.g., homeassistant/fan/office/set). When a payload of ON arrives, the callback triggers fan.write(1). Because Node.js is inherently event-driven, the MQTT listener and the GPIO state machine will run concurrently without blocking each other, making it vastly superior to synchronous Python scripts for this specific architecture.
Frequently Asked Questions
Is Node.js for Raspberry Pi 5 GPIO different than Pi 4?
Yes, fundamentally. The Raspberry Pi 4 uses the BCM2711 SoC, which exposes GPIO via the legacy Linux sysfs interface (compatible with the onoff library). The Raspberry Pi 5 uses the external RP1 I/O controller, which requires the modern libgpiod character device interface. If you are writing Node.js for Raspberry Pi 5, you must use a gpiod-compatible npm package like node-gpiod, as older libraries will throw ENOENT errors.
Why does my Node.js script exit immediately after setting the GPIO high?
Node.js is asynchronous and event-driven. If your script only contains a fan.writeSync(1) command and nothing else, the V8 engine reaches the end of the call stack, determines there are no pending events or timers in the event loop, and cleanly exits the process. To keep it running, you must introduce a persistent event listener (like an HTTP server or MQTT client) or a setInterval timer.
Can I use Node.js for Raspberry Pi to read analog sensors?
Not natively. The Raspberry Pi (all models, including the Pi 5) does not have a built-in Analog-to-Digital Converter (ADC). To read analog sensors (like an LDR or potentiometer) with Node.js, you must wire an external ADC module, such as the MCP3008, via the SPI bus, or use an I2C ADC like the ADS1115. You can then use the spi-device or i2c-bus npm packages to read the digital conversions.
How much memory does a Node.js GPIO script consume on a Pi Zero 2 W?
A bare-bones Node.js script using the onoff library typically idles around 25MB to 35MB of RAM. On a Raspberry Pi Zero 2 W (which has 512MB of RAM), this is perfectly manageable. However, if you add heavy frameworks like Express.js or load large JSON payloads into memory, you can easily spike past 100MB. For highly constrained environments, consider stripping out V8 overhead by using C++ via Node-API, or switching to a lightweight runtime like MicroPython.






