node-gyp vs prebuild vs @napi-rs/cli: Native Node.js Addons in 2026
TL;DR
node-gyp is the original build tool for native Node.js addons — compiles C/C++ using GYP (Google's build system) and requires Python + build tools on every install. prebuild generates and distributes pre-compiled binaries so users don't need to compile — it wraps around node-gyp but makes installation painless. @napi-rs/cli is the modern approach — write Rust, compile to N-API binaries with automated cross-compilation and pre-built distribution. In 2026, the best-in-class approach is N-API + prebuild (for C/C++) or @napi-rs (for Rust). Avoid requiring users to compile from source.
Key Takeaways
- node-gyp: ~40M weekly downloads — the standard C/C++ compiler, requires Python + build tools to install
- prebuild: ~3M weekly downloads — distributes pre-built binaries, wraps node-gyp
- @napi-rs/cli: ~500K weekly downloads — Rust-based, N-API, automated cross-compilation and publishing
- N-API (Node.js API) is ABI-stable — addons built for Node.js 18 work in 20, 22, etc.
- Users should never need to compile from source — always ship pre-built binaries
- NEON (Rust) and @napi-rs are the modern path for new native addons
Why Native Addons?
Use native addons when JavaScript isn't fast enough:
- CPU-intensive computation: image processing, video encoding, ML inference
- Accessing OS APIs: system notifications, hardware access, raw sockets
- Integrating existing C/C++ libraries: SQLite, LevelDB, OpenSSL
- Cryptography: hardware-accelerated AES, SHA
- Media processing: sharp (libvips), canvas (Cairo/Skia)
Popular native Node.js packages:
sharp — libvips image processing
canvas — Cairo rendering
bcrypt — password hashing
sqlite3 — SQLite database
@napi-rs/canvas — Skia rendering (Rust/N-API)
duckdb — DuckDB analytics engine
leveldown — LevelDB bindings
node-gyp
node-gyp — the standard C/C++ addon compiler:
binding.gyp (the build file)
{
"targets": [
{
"target_name": "myaddon",
"sources": ["src/addon.cc"],
"include_dirs": [
"<!@(node -p \"require('node-addon-api').include\")"
],
"dependencies": [
"<!(node -p \"require('node-addon-api').gyp\")"
],
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
}
]
}
N-API C++ addon (modern, stable ABI)
// src/addon.cc
#include <napi.h>
// Function that does some computation:
Napi::Value CalculateHealthScore(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
if (info.Length() < 2 || !info[0].IsNumber() || !info[1].IsNumber()) {
Napi::TypeError::New(env, "Expected two numbers").ThrowAsJavaScriptException();
return env.Null();
}
double downloads = info[0].As<Napi::Number>().DoubleValue();
double stars = info[1].As<Napi::Number>().DoubleValue();
// Native computation (fast):
double score = (downloads * 0.6 + stars * 0.4) / 1000.0;
double clamped = score > 100 ? 100 : score;
return Napi::Number::New(env, clamped);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("calculateHealthScore",
Napi::Function::New(env, CalculateHealthScore));
return exports;
}
NODE_API_MODULE(myaddon, Init)
Build and use
# Build:
node-gyp configure
node-gyp build
# Or: npm run build (uses node-gyp through package.json scripts)
// Load the native addon:
const addon = require("./build/Release/myaddon.node")
const score = addon.calculateHealthScore(45_000_000, 200_000)
console.log(score) // Native computation result
The problem node-gyp creates for users
# When users install a package with node-gyp, they see this:
npm install bcrypt
# npm WARN optional Skipping failed optional dependency /chokidar/node_modules/fsevents
# gyp ERR! build error
# gyp ERR! stack Error: not found: make
# gyp ERR! stack at getNotFoundError (/usr/lib/node_modules/npm/node_modules/which/which.js:14:12)
# gyp ERR! System Linux 5.15.0
# Fix: apt-get install build-essential python3
# This is the user experience problem that prebuild and @napi-rs solve
prebuild
prebuild — distribute pre-built binaries:
How prebuild works
# Package author runs prebuild on CI/CD for each platform:
npx prebuild --runtime node --target 20.0.0 # Node.js 20, current arch
npx prebuild --runtime node --target 22.0.0 # Node.js 22
# Uploads to GitHub Releases as assets:
npx prebuild --upload-all GITHUB_TOKEN
# Creates: prebuilds/linux-x64/myaddon.node, prebuilds/darwin-arm64/myaddon.node, etc.
package.json setup
{
"scripts": {
"install": "prebuild-install || node-gyp rebuild",
"prebuild": "prebuild --strip --verbose"
},
"dependencies": {
"prebuild-install": "^7.0.0",
"node-gyp": "^9.0.0",
"node-addon-api": "^7.0.0"
}
}
User install experience (with prebuild)
# User installs the package:
npm install my-native-package
# prebuild-install downloads the pre-built binary from GitHub Releases
# No compilation needed — fast, no Python, no build tools required!
# Only falls back to compilation if:
# 1. No pre-built binary for their platform/Node.js version
# 2. BUILDING_FROM_SOURCE env var is set
@napi-rs/cli (Rust-based N-API)
@napi-rs — write native addons in Rust:
Why Rust for native addons?
Rust advantages over C/C++:
- No manual memory management (no use-after-free, buffer overflows)
- Better error messages and tooling
- cargo makes dependency management easy
- async/await support via tokio
- Cross-compilation is first-class
@napi-rs advantages:
- TypeScript types generated automatically from Rust function signatures
- GitHub Actions templates for cross-compilation to all platforms
- Pre-built binary distribution built-in
- No binding.gyp files — just Cargo.toml
Create a new @napi-rs project
# Scaffold a new native addon:
npm init @napi-rs my-addon
cd my-addon
# Project structure:
# ├── Cargo.toml — Rust dependencies
# ├── src/lib.rs — Rust code
# ├── package.json — npm package
# ├── index.js — JS wrapper
# ├── index.d.ts — TypeScript types (auto-generated)
# └── .github/workflows/CI.yml — Cross-platform build + publish
Rust implementation
// src/lib.rs
use napi::bindgen_prelude::*;
use napi_derive::napi;
// #[napi] macro generates N-API bindings automatically:
#[napi]
pub fn calculate_health_score(downloads: f64, stars: f64) -> f64 {
let score = (downloads * 0.6 + stars * 0.4) / 1000.0;
score.min(100.0)
}
// Async function (runs on Tokio thread pool, non-blocking):
#[napi]
pub async fn process_packages(packages: Vec<String>) -> Result<Vec<f64>> {
// Expensive computation in Rust thread pool — doesn't block Node.js event loop
let scores: Vec<f64> = packages.iter()
.map(|pkg| compute_score(pkg))
.collect();
Ok(scores)
}
// TypeScript class:
#[napi]
pub struct PackageAnalyzer {
threshold: f64,
}
#[napi]
impl PackageAnalyzer {
#[napi(constructor)]
pub fn new(threshold: f64) -> Self {
PackageAnalyzer { threshold }
}
#[napi]
pub fn is_healthy(&self, score: f64) -> bool {
score >= self.threshold
}
}
Auto-generated TypeScript types
// index.d.ts — auto-generated from Rust code:
/** Calculate health score from downloads and stars */
export declare function calculateHealthScore(downloads: number, stars: number): number
/** Process packages asynchronously (runs on Rust thread pool) */
export declare function processPackages(packages: Array<string>): Promise<Array<number>>
/** Package analyzer with configurable threshold */
export declare class PackageAnalyzer {
constructor(threshold: number)
isHealthy(score: number): boolean
}
Cross-compilation CI
# .github/workflows/CI.yml (generated by @napi-rs scaffold)
name: CI
on: [push, pull_request]
jobs:
build:
strategy:
matrix:
settings:
- host: ubuntu-latest
target: x86_64-unknown-linux-gnu
- host: ubuntu-latest
target: aarch64-unknown-linux-gnu
cross: true
- host: macos-latest
target: x86_64-apple-darwin
- host: macos-latest
target: aarch64-apple-darwin
- host: windows-latest
target: x86_64-pc-windows-msvc
steps:
- uses: actions/checkout@v4
- uses: napi-rs/setup-rust@v1
- run: yarn build --target ${{ matrix.settings.target }}
- uses: actions/upload-artifact@v4
with:
name: bindings-${{ matrix.settings.target }}
path: "*.node"
Feature Comparison
| Feature | node-gyp | prebuild | @napi-rs |
|---|---|---|---|
| Language | C/C++ | C/C++ | Rust |
| Pre-built binaries | ❌ | ✅ | ✅ |
| Cross-compilation | Manual | Manual | ✅ Built-in |
| TypeScript types | Manual | Manual | ✅ Auto-generated |
| ABI stability (N-API) | ✅ if using N-API | ✅ | ✅ |
| User compile required | ✅ | ❌ | ❌ |
| Memory safety | Manual | Manual | ✅ Rust |
| Async/await | Complex | Complex | ✅ Native |
| Ecosystem maturity | ✅ Battle-tested | ✅ | Growing fast |
| Setup complexity | Low | Medium | Low (scaffold) |
When to Use Each
Choose node-gyp if:
- Maintaining an existing C/C++ addon
- Wrapping an existing C library with no Rust FFI available
- The addon will always be compiled locally (internal tool)
Choose prebuild if:
- Publishing a C/C++ addon that users will install
- You want fast installs with no compilation for end users
- Maintaining existing node-gyp codebase but need better distribution
Choose @napi-rs if:
- Writing a new native addon from scratch in 2026
- Want memory-safe native code without C/C++ footguns
- Need async operations that don't block the Node.js event loop
- Want TypeScript types auto-generated from Rust code
Methodology
Download data from npm registry (weekly average, February 2026). Feature comparison based on node-gyp v9.x, prebuild v12.x, and @napi-rs/cli v2.x.