Native Modules
N-API and native addon patterns for extending Node.js with high-performance C/C++ and Rust modules
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 linesNative 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
prebuildifyso users do not need a C/C++ compiler installed. - Run CPU-intensive native code on a worker thread (via
Napi::AsyncWorkerornapi-rsasync functions) to avoid blocking the event loop. - Validate all JavaScript arguments in the native layer — incorrect types cause segfaults, not exceptions.
- Use
Napi::ObjectWrapfor stateful objects that need to expose methods and properties to JavaScript.
Common Pitfalls
- Accessing Napi objects from a worker thread —
Napi::Value,Napi::Object, and all JS handles are only valid on the main thread. Accessing them fromExecute()causes crashes. - Forgetting to handle exceptions — unless
NAPI_DISABLE_CPP_EXCEPTIONSis defined, C++ exceptions in callbacks propagate to JavaScript. With exceptions disabled, you must checkenv.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-gypmust 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
Related Skills
Child Processes
Child process management patterns for spawning, communicating with, and controlling external processes
Clustering
Cluster module patterns for scaling Node.js applications across multiple CPU cores
Error Handling
Comprehensive error handling strategies for robust and debuggable Node.js applications
Event Emitter
EventEmitter patterns for building decoupled, event-driven architectures in Node.js
File System
Modern fs/promises patterns for safe, efficient file system operations in Node.js
Streams
Node.js streams for efficient memory-conscious data processing with backpressure handling