Skip to content

Node.js N-API Addons — Complete Guide to Native C/C++ Extensions

DodaTech Updated 2026-06-28 4 min read

In this tutorial, you will learn about Node.js N. We cover key concepts, practical examples, and best practices to help you master this topic.

Node.js N-API addons allow writing native C or C++ modules that integrate directly with the Node.js runtime, providing performance-critical operations with stable ABI across Node versions.

What You'll Learn

By the end of this tutorial, you'll create N-API addons using the C API and node-addon-api C++ wrapper, compile with node-gyp, handle JavaScript values, and expose native functions.

Why N-API Matters

N-API is the only stable ABI for Node.js native addons. Code compiled against N-API works across Node versions without recompilation, unlike the older Nan API.

Real-World Use

A video processing library uses an N-API addon to perform pixel-level operations in C++ at 60fps, calling back into JavaScript for higher-level logic like scene detection and metadata extraction.

N-API Path

flowchart LR
  A[Architecture] --> B[N-API Addons]
  B --> C[Worker Threads]
  C --> D[Performance]
  D --> E[Profiling]
  B --> F{You Are Here}
  style F fill:#f90,color:#fff

Project Setup

N-API addons require node-gyp for compilation. Create a binding.gyp file and a C++ source file.

// binding.gyp
{
  "targets": [{
    "target_name": "addon",
    "sources": ["addon.cc"],
    "include_dirs": ["<!(node -e \"require('node-addon-api').include\")"],
    "dependencies": ["<!(node -e \"require('node-addon-api').gyp\")"],
    "defines": ["NAPI_VERSION=9"]
  }]
}
// Build: node-gyp configure && node-gyp build

Basic N-API Module

Use node-addon-api (C++) for a cleaner API. Expose functions that return JavaScript values.

#include <napi.h>
Napi::String Hello(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  return Napi::String::New(env, "Hello from C++ addon!");
}
Napi::Object Init(Napi::Env env, Napi::Object exports) {
  exports.Set("hello", Napi::Function::New(env, Hello));
  return exports;
}
NODE_API_MODULE(NODE_GYP_MODULE_NAME, Init)

Using the Addon from JavaScript

Load the compiled addon with require and call native functions.

const addon = require("./build/Release/addon");
console.log(addon.hello());
// Hello from C++ addon!

Passing JavaScript Values

Handle numbers, strings, booleans, arrays, and objects in C++.

Napi::Value ProcessArray(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  if (!info[0].IsArray()) {
    Napi::TypeError::New(env, "Expected array").ThrowAsJavaScriptException();
    return env.Undefined();
  }
  Napi::Array arr = info[0].As<Napi::Array>();
  uint32_t len = arr.Length();
  Napi::Array result = Napi::Array::New(env, len);
  for (uint32_t i = 0; i < len; i++) {
    Napi::Value val = arr.Get(i);
    if (val.IsNumber()) {
      result.Set(i, Napi::Number::New(env, val.As<Napi::Number>().DoubleValue() * 2));
    }
  }
  return result;
}

Synchronous vs Asynchronous Operations

Perform expensive operations on the libuv thread pool to avoid blocking the event loop.

class AsyncWorker : public Napi::AsyncWorker {
public:
  AsyncWorker(Napi::Function& callback)
    : Napi::AsyncWorker(callback) {}
  void Execute() override {
    // Heavy computation here (runs on thread pool)
    result = performExpensiveOperation();
  }
  void OnOK() override {
    Callback().Call({Env().Undefined(), Napi::Number::New(Env(), result)});
  }
private:
  int result;
};

Common Mistakes

1. Blocking the Event Loop in Native Code

Synchronous C++ operations block the main thread. Use AsyncWorker to run on the libuv thread pool.

2. Memory Leaks in Native Modules

N-API handles are automatically managed. Raw C++ allocations must be freed manually. Use RAII patterns.

3. Incorrect Type Checking

Always check JavaScript value types with IsNumber(), IsString(), etc. before conversion. Incorrect types crash the Process.

4. Ignoring Error Handling

Call ThrowAsJavaScriptException to propagate errors. Native crashes become uncaught exceptions.

5. Forgetting to Rebuild After Node Upgrade

N-API is ABI-stable, but recompile when upgrading the node-addon-api package or major Node versions.

Practice Questions

1. What is the main advantage of N-API over Nan?

N-API provides a stable ABI. Addons compiled against N-API work across Node versions without recompilation.

2. How do you compile an N-API addon?

Use node-gyp: node-gyp configure && node-gyp build. A binding.gyp file specifies sources and dependencies.

3. How do you run native code without blocking the event loop?

Use Napi::AsyncWorker to execute code on the libuv thread pool, then call back into JavaScript.

4. What is the role of binding.gyp?

It is a JSON-like configuration file that tells node-gyp how to compile the native module.

5. Challenge: Create an N-API addon that calculates Fibonacci numbers.

// C++ function calculates fib(n) synchronously
// JavaScript call: const result = addon.fibonacci(40);

FAQ

Is N-API stable across Node.js versions?

Yes. N-API guarantees ABI stability starting from Node 8. Code compiled once works across versions.

What is the difference between N-API and node-addon-api?

N-API is the C API. node-addon-api is a C++ wrapper that provides a more ergonomic interface.

Can I use ES modules with N-API addons?

Yes. Load the addon with createRequire or use dynamic import with .node file extensions.

How do I debug native addons?

Use gdb or lldb with node. Build with --debug flag for symbols.

Are N-API addons portable across platforms?

Yes. node-gyp handles platform-specific compilation. The same source works on Linux, macOS, and Windows.

Mini Project: String Processing Addon

Build a native addon that counts word frequency efficiently.

#include <napi.h>
#include <unordered_map>
#include <sstream>
Napi::Object WordCount(const Napi::CallbackInfo& info) {
  Napi::Env env = info.Env();
  std::string text = info[0].As<Napi::String>().Utf8Value();
  std::unordered_map<std::string, int> counts;
  std::istringstream stream(text);
  std::string word;
  while (stream >> word) counts[word]++;
  Napi::Object result = Napi::Object::New(env);
  for (auto& [word, count] : counts) {
    result.Set(Napi::String::New(env, word), Napi::Number::New(env, count));
  }
  return result;
}

What's Next

Node.js Worker Threads Node.js Performance Node.js Profiling

Built by the developers of DodaTech

Doda Browser, DodaZIP & Durga Antivirus Pro