Skip to main content

node-gyp vs prebuild vs @napi-rs/cli: Native Node.js Addons in 2026

·PkgPulse Team

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

Featurenode-gypprebuild@napi-rs
LanguageC/C++C/C++Rust
Pre-built binaries
Cross-compilationManualManual✅ Built-in
TypeScript typesManualManual✅ Auto-generated
ABI stability (N-API)✅ if using N-API
User compile required
Memory safetyManualManual✅ Rust
Async/awaitComplexComplex✅ Native
Ecosystem maturity✅ Battle-testedGrowing fast
Setup complexityLowMediumLow (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.

Compare build tooling and native packages on PkgPulse →

Comments

Stay Updated

Get the latest package insights, npm trends, and tooling tips delivered to your inbox.