Many popular packages (database drivers, image libraries, bcrypt) are native addons; you need to debug, ship and occasionally write them.
A native addon is a shared library (.node file) that Node loads like a module. It lets JavaScript call C, C++ or Rust code: to reuse an existing library (libvips, SQLite), to reach OS APIs Node doesn't expose, or occasionally for raw speed.
Modern addons use Node-API (N-API), a stable C ABI maintained by Node itself. Its key property is ABI stability: an addon compiled against Node-API version N runs on every later Node version without recompiling. Older addons that used V8 and NAN directly had to be rebuilt for every Node major. Wrappers make Node-API pleasant: node-addon-api for C++, napi-rs for Rust.
Building and shipping:
node-gyp(with abinding.gyp) orcmake-jscompile the addon; this needs Python and a C/C++ toolchain on the machine.- Because users rarely have compilers, packages ship prebuilt binaries per platform/architecture — via
prebuildify, or separate optional-dependency packages per platform (the approach napi-rs uses). - In ESM, load a
.nodefile withcreateRequire(import.meta.url)andrequire().
Writing one, the rules that bite:
- JavaScript values are only valid within a handle scope; keep long-lived ones as references.
- Never block the main thread: long work belongs in an
AsyncWorkerrunning on the libuv thread pool. - Calling back into JS from other threads requires a thread-safe function.
- An addon can be loaded by several worker threads at once, so keep global state per instance (
napi_set_instance_data), not in C++ globals.
Before reaching for an addon, consider WebAssembly: portable, sandboxed, no compile step for users, and often fast enough. Many former native packages now ship WASM builds.
Example
// hello.cc — a minimal Node-API addon using node-addon-api (C++)
#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, "two numbers expected").ThrowAsJavaScriptException();
return env.Null();
}
double sum = info[0].As<Napi::Number>().DoubleValue() + info[1].As<Napi::Number>().DoubleValue();
return Napi::Number::New(env, sum);
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
exports.Set("add", Napi::Function::New(env, Add));
return exports;
}
NODE_API_MODULE(hello, Init)
// Build: npm i node-addon-api && npx node-gyp configure build
// Use from ESM:
// import { createRequire } from "node:module";
// const { add } = createRequire(import.meta.url)("./build/Release/hello.node");
// add(2, 3) // 5Remember
- Node-API gives a stable ABI: build once, run on later Node versions
- Ship prebuilt binaries; users shouldn't need compilers
- Do long work in AsyncWorker; use thread-safe functions for cross-thread callbacks
- Consider WebAssembly before writing an addon
Watch out for
- Doing heavy work synchronously inside an addon function, blocking the event loop.