Skip to main content
Technology & EngineeringNodejs Patterns231 lines

Native Modules

N-API and native addon patterns for extending Node.js with high-performance C/C++ and Rust modules

Quick Summary33 lines
You are an expert in building Node.js native addons using N-API (Node-API) and node-addon-api for high-performance extensions in C, C++, and Rust.

## Key Points

- **node-gyp** — the default build system, uses Python and GYP files. Works everywhere but is slow and complex to configure.
- **cmake-js** — alternative using CMake, better IDE integration and cross-compilation.
- **prebuildify / prebuild** — pre-compile binaries for multiple platforms and ship them in the npm package, so users never need a compiler.
- Use Node-API (not raw V8 API) for all new addons — it guarantees ABI stability across Node.js versions.
- Ship prebuilt binaries with `prebuildify` so users do not need a C/C++ compiler installed.
- Run CPU-intensive native code on a worker thread (via `Napi::AsyncWorker` or `napi-rs` async functions) to avoid blocking the event loop.
- Validate all JavaScript arguments in the native layer — incorrect types cause segfaults, not exceptions.
- Use `Napi::ObjectWrap` for stateful objects that need to expose methods and properties to JavaScript.
- **Accessing Napi objects from a worker thread** — `Napi::Value`, `Napi::Object`, and all JS handles are only valid on the main thread. Accessing them from `Execute()` causes crashes.
- **Memory leaks from prevented GC** — storing persistent references (`Napi::Reference`) without releasing them prevents JavaScript objects from being garbage collected.
- **ABI breaks from V8 headers** — addons that include V8 headers directly (`v8.h`) break on every Node.js major version. Always use Node-API instead.

## Quick Example

```javascript
// index.js — consuming the Rust addon
const { add, hashFile } = require('./binding.node');

console.log(add(2, 3)); // 5
const hash = await hashFile('/path/to/file');
```

```javascript
// lib/index.js — load prebuilt binary or fall back to build
const binding = require('node-gyp-build')(__dirname);
module.exports = binding;
```
skilldb get nodejs-patterns-skills/Native ModulesFull skill: 231 lines
Paste into your CLAUDE.md or agent config

Native Modules — Node.js Patterns

You are an expert in building Node.js native addons using N-API (Node-API) and node-addon-api for high-performance extensions in C, C++, and Rust.

Core Philosophy

Overview

Native addons let Node.js call into compiled C, C++, or Rust code for performance-critical operations — image processing, cryptography, machine learning inference, and hardware access. Node-API (formerly N-API) provides a stable, ABI-compatible C interface that does not break across Node.js major versions. The node-addon-api package provides a C++ wrapper over Node-API for ergonomic development. Rust developers use the napi-rs crate for safe, idiomatic bindings.

Core Concepts

Node-API (N-API)

A C API embedded in Node.js that abstracts away V8 internals. Addons built against Node-API are binary-compatible across Node.js versions — no recompilation required. Node-API is versioned independently (currently up to version 9).

node-addon-api

A header-only C++ library that wraps Node-API in idiomatic C++ classes (Napi::Value, Napi::Object, Napi::Function). It eliminates manual napi_value handle management and error checking.

node-gyp vs cmake-js vs prebuild

  • node-gyp — the default build system, uses Python and GYP files. Works everywhere but is slow and complex to configure.
  • cmake-js — alternative using CMake, better IDE integration and cross-compilation.
  • prebuildify / prebuild — pre-compile binaries for multiple platforms and ship them in the npm package, so users never need a compiler.

napi-rs

A Rust crate for building Node-API-compatible addons. Provides #[napi] macros for automatic binding generation, memory safety, and excellent developer experience.

Implementation Patterns

Basic C++ addon with node-addon-api

// src/addon.cc
#include <napi.h>

Napi::Value Add(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 a = info[0].As<Napi::Number>().DoubleValue();
  double b = info[1].As<Napi::Number>().DoubleValue();

  return Napi::Number::New(env, a + b);
}

Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("add", Napi::Function::New(env, Add));
  return exports;
}

NODE_API_MODULE(addon, Init)
// binding.gyp
{
  "targets": [{
    "target_name": "addon",
    "sources": ["src/addon.cc"],
    "include_dirs": [
      "<!@(node -p \"require('node-addon-api').include\")"
    ],
    "defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
  }]
}

Async worker for non-blocking operations

#include <napi.h>

class HashWorker : public Napi::AsyncWorker {
 public:
  HashWorker(Napi::Function& callback, std::string data)
      : Napi::AsyncWorker(callback), data_(std::move(data)) {}

  void Execute() override {
    // Runs on a libuv thread, NOT the main thread
    // Do NOT access Napi objects here
    result_ = compute_sha256(data_);
  }

  void OnOK() override {
    // Back on the main thread — safe to create JS values
    Callback().Call({Env().Null(), Napi::String::New(Env(), result_)});
  }

 private:
  std::string data_;
  std::string result_;
};

Napi::Value HashAsync(const Napi::CallbackInfo& info) {
  std::string data = info[0].As<Napi::String>().Utf8Value();
  Napi::Function callback = info[1].As<Napi::Function>();

  auto* worker = new HashWorker(callback, data);
  worker->Queue();
  return info.Env().Undefined();
}

Promise-based async with Napi::AsyncWorker

class ComputeWorker : public Napi::AsyncWorker {
 public:
  ComputeWorker(Napi::Env env, Napi::Promise::Deferred deferred, int input)
      : Napi::AsyncWorker(env), deferred_(deferred), input_(input) {}

  void Execute() override {
    result_ = heavy_computation(input_);
  }

  void OnOK() override {
    deferred_.Resolve(Napi::Number::New(Env(), result_));
  }

  void OnError(const Napi::Error& err) override {
    deferred_.Reject(err.Value());
  }

 private:
  Napi::Promise::Deferred deferred_;
  int input_;
  double result_;
};

Napi::Value ComputeAsync(const Napi::CallbackInfo& info) {
  int input = info[0].As<Napi::Number>().Int32Value();
  auto deferred = Napi::Promise::Deferred::New(info.Env());
  auto* worker = new ComputeWorker(info.Env(), deferred, input);
  worker->Queue();
  return deferred.Promise();
}

Rust addon with napi-rs

// src/lib.rs
use napi_derive::napi;

#[napi]
pub fn add(a: f64, b: f64) -> f64 {
    a + b
}

#[napi]
pub async fn hash_file(path: String) -> napi::Result<String> {
    let data = tokio::fs::read(&path)
        .await
        .map_err(|e| napi::Error::from_reason(format!("Read error: {e}")))?;

    let hash = sha2::Sha256::digest(&data);
    Ok(format!("{hash:x}"))
}
// index.js — consuming the Rust addon
const { add, hashFile } = require('./binding.node');

console.log(add(2, 3)); // 5
const hash = await hashFile('/path/to/file');

Using prebuildify for distribution

{
  "scripts": {
    "build": "node-gyp rebuild",
    "prebuild": "prebuildify --napi --strip",
    "install": "node-gyp-build"
  },
  "dependencies": {
    "node-gyp-build": "^4.8.0"
  },
  "devDependencies": {
    "prebuildify": "^6.0.0"
  }
}
// lib/index.js — load prebuilt binary or fall back to build
const binding = require('node-gyp-build')(__dirname);
module.exports = binding;

Best Practices

  • Use Node-API (not raw V8 API) for all new addons — it guarantees ABI stability across Node.js versions.
  • Ship prebuilt binaries with prebuildify so users do not need a C/C++ compiler installed.
  • Run CPU-intensive native code on a worker thread (via Napi::AsyncWorker or napi-rs async functions) to avoid blocking the event loop.
  • Validate all JavaScript arguments in the native layer — incorrect types cause segfaults, not exceptions.
  • Use Napi::ObjectWrap for stateful objects that need to expose methods and properties to JavaScript.

Common Pitfalls

  • Accessing Napi objects from a worker threadNapi::Value, Napi::Object, and all JS handles are only valid on the main thread. Accessing them from Execute() causes crashes.
  • Forgetting to handle exceptions — unless NAPI_DISABLE_CPP_EXCEPTIONS is defined, C++ exceptions in callbacks propagate to JavaScript. With exceptions disabled, you must check env.IsExceptionPending() after every call.
  • Memory leaks from prevented GC — storing persistent references (Napi::Reference) without releasing them prevents JavaScript objects from being garbage collected.
  • ABI breaks from V8 headers — addons that include V8 headers directly (v8.h) break on every Node.js major version. Always use Node-API instead.
  • Missing prebuild for the user's platform — if no prebuilt binary matches, node-gyp must compile from source, which fails when build tools are not installed. Test prebuilds for all supported platforms in CI.

Anti-Patterns

Over-engineering for hypothetical scale. Building for millions of users when you have hundreds adds complexity without value. Solve today's problems first.

Ignoring the existing ecosystem. Reinventing functionality that mature libraries already provide well wastes time and introduces unnecessary risk.

Premature abstraction. Creating elaborate frameworks and utilities before you have enough concrete cases to know what the abstraction should look like produces the wrong abstraction.

Neglecting error handling at boundaries. Internal code can trust its inputs, but system boundaries (user input, APIs, file I/O) require defensive validation.

Skipping documentation for obvious code. What is obvious to you today will not be obvious to your colleague next month or to you next year.

Install this skill directly: skilldb add nodejs-patterns-skills

Get CLI access →