Native Addons (N-API)
Native addons are C or C++ modules that compile to .node binary files and are loaded by Node.js like any other module. They're the bridge between JavaScript and native code: you write performance-critical or hardware-interfacing logic in C/C++, compile it to a native addon, and call it from JavaScript with a normal require(). N-API (Node-API) is the stable C API layer that insulates addon code from V8 internals — an addon written against N-API compiles once and runs across Node versions without recompilation, unlike the older nan (Native Abstractions for Node.js) approach.
When native addons are worth it
Good reason | Poor reason |
|---|---|
Wrapping an existing C/C++ library (libsodium, OpenCV, SQLite) | JavaScript is slightly slower — profile first, optimize JS first |
CPU-bound work that saturates JavaScript (image processing, codecs, compression) | Premature optimization without measurement |
Hardware access (serial ports, GPU APIs, custom drivers) | Not knowing about WebAssembly, which is often a simpler path |
Algorithmic hot paths that truly need pointer-level memory control | Complexity aversion — N-API is complex; WebAssembly may be simpler |
The N-API architecture
JavaScript (your Node.js code)
│
│ require('./build/Release/my-addon.node')
▼
N-API layer (stable C ABI — does NOT change between Node versions)
│
│ napi_create_function, napi_get_value_int32, napi_call_function ...
▼
Your C/C++ addon code (my-addon.cc)
│
│ calls into native libraries, OS APIs, hardware
▼
Native libraries / OSSetting up a native addon project
npm install --save-dev node-gyp npm install node-addon-api # C++ wrapper over raw N-API (much easier to use)
// package.json — scripts to build the addon:
{
"scripts": {
"build": "node-gyp rebuild",
"install": "node-gyp rebuild" // auto-build on npm install
}
}# binding.gyp — build configuration (GYP format):
{
"targets": [{
"target_name": "my_addon",
"sources": ["src/my-addon.cc"],
"include_dirs": [
"<!@(node -p "require('node-addon-api').include")"
],
"defines": ["NAPI_DISABLE_CPP_EXCEPTIONS"]
}]
}Writing a simple addon with node-addon-api
// src/my-addon.cc
#include <napi.h>
// A function that adds two numbers — trivial but shows the pattern:
Napi::Value Add(const Napi::CallbackInfo& info) {
Napi::Env env = info.Env();
// Validate argument count and types:
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);
}
// Module initialization — called once when the module is loaded:
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("add", Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(my_addon, Init)// Loading and using the compiled addon:
// eslint-disable-next-line @typescript-eslint/no-require-imports
const addon = require('./build/Release/my_addon')
console.log(addon.add(3, 4)) // 7Async addons — keeping the event loop free
// Async worker pattern — runs C++ work on the libuv thread pool, not the event loop:
#include <napi.h>
class HashWorker : public Napi::AsyncWorker {
public:
HashWorker(Napi::Function& callback, std::string input)
: Napi::AsyncWorker(callback), input_(std::move(input)) {}
// Execute() runs on the thread pool — NO JavaScript calls allowed here:
void Execute() override {
result_ = computeExpensiveHash(input_); // pure C++ work
}
// OnOK() runs back on the main thread — safe to call JavaScript:
void OnOK() override {
Napi::HandleScope scope(Env());
Callback().Call({Env().Null(), Napi::String::New(Env(), result_)});
}
private:
std::string input_;
std::string result_;
std::string computeExpensiveHash(const std::string& s) { /* ... */ return s; }
};
Napi::Value ComputeHash(const Napi::CallbackInfo& info) {
std::string input = info[0].As<Napi::String>();
Napi::Function callback = info[1].As<Napi::Function>();
auto* worker = new HashWorker(callback, input);
worker->Queue(); // schedule on thread pool
return info.Env().Undefined();
}Distributing addons — prebuilt binaries
# Building requires a C++ compiler and Python on each machine — painful for npm install. # Use node-pre-gyp or prebuildify to publish prebuilt binaries: npm install --save-dev prebuildify # Builds .node files for multiple platforms/architectures # Uploads to GitHub Releases; downloaded at install time on target machine # package.json: # "install": "node-pre-gyp install --fallback-to-build" # This downloads a prebuilt binary if available, or falls back to compiling from source.