Native addons and Node-API

Node.js · Expert · 18 min

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 a binding.gyp) or cmake-js compile 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 .node file with createRequire(import.meta.url) and require().

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 AsyncWorker running 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) // 5

Remember

  • 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.

Check yourself

  1. Why did the move from NAN to Node-API matter so much for addon users?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: nodejs.org/docs/latest-v22.x/api/n-api.html