Mastering NAPI‑RS: Build Cross‑Platform Node.js Native Addons with Rust

This article introduces NAPI‑RS, a Rust‑based framework for creating pre‑compiled Node.js native extensions, covering project setup, multi‑platform distribution, type generation, module registration, advanced usage patterns, and debugging techniques to help developers efficiently bridge Rust and JavaScript.

Alipay Experience Technology
Alipay Experience Technology
Alipay Experience Technology
Mastering NAPI‑RS: Build Cross‑Platform Node.js Native Addons with Rust

How to integrate

Use @napi-rs/cli to initialize a project or start from the napi-rs/package-template GitHub template. The framework provides automated multi‑platform compilation and publishing solutions.

Generated project directory structure

.
├── Cargo.lock
├── Cargo.toml
├── README.md
├── __tests__
│   └── index.spec.mjs
├── build.rs
├── html-text-content.darwin-arm64.node
├── index.d.ts // generated from Rust types
├── index.js // loads the appropriate .node file based on platform
├── npm
│   ├── android-arm-eabi
│   │   ├── README.md
│   │   └── package.json
│   ├── android-arm64
│   │   ├── README.md
│   │   └── package.json
│   ├── darwin-arm64
│   │   ├── README.md
│   │   └── package.json
│   ├── darwin-x64
│   │   ├── README.md
│   │   └── package.json
│   ├── freebsd-x64
│   │   ├── README.md
│   │   └── package.json
│   ├── linux-arm-gnueabihf
│   │   ├── README.md
│   │   └── package.json
│   ├── linux-arm64-gnu
│   │   ├── README.md
│   │   └── package.json
│   ├── linux-arm64-musl
│   │   ├── README.md
│   │   └── package.json
│   ├── linux-x64-gnu
│   │   ├── README.md
│   │   └── package.json
│   ├── linux-x64-musl
│   │   ├── README.md
│   │   └── package.json
│   ├── win32-arm64-msvc
│   │   ├── README.md
│   │   └── package.json
│   ├── win32-ia32-msvc
│   │   ├── README.md
│   │   └── package.json
│   └── win32-x64-msvc
│       ├── README.md
│       └── package.json
├── package.json
├── rustfmt.toml
└── src
    └── lib.rs // Rust source

Automatic platform distribution

The framework assigns each triple (os, cpu, libc) an npm package and pre‑compiles the corresponding .node file, which is exported via the main field. When the main package is installed, npm pulls the optional dependency matching the current machine, e.g., @node-rs/xxhash-darwin-arm64 on macOS.

Some usage

Automatic d.ts generation

NAPI‑RS can generate TypeScript declaration files from Rust types, providing type support for the binding library.

Naming conversion

Rust snake_case identifiers are converted to camelCase in JavaScript (e.g., hello_worldhelloWorld). The #[napi(js_name = "yourFnName")] attribute overrides the generated name.

JS type mapping

bindgen_prelude

Supported basic JS types include Undefined, Null, Number/BigInt, String, Boolean, Buffer, Object, Array, and TypedArray.

Working mechanism

Module registration

#[napi] macro automatically generates module exports; v2 simplifies the syntax compared to v1.
use napi_derive::napi;

#[napi]
pub fn plus_100(input: u32, input1: u32) -> u32 {
  input + input1 + 100
}

The generated plus_100 function is registered with register_module_export, which stores a pointer in a local thread queue. When the addon loads, Node calls napi_register_module_v1, which iterates the queue, registers the function via register_js_function, and attaches it to the exports object.

{
  "name": "@node-rs/xxhash",
  "version": "1.3.0",
  "description": "Fastest xxhash implementation in Node.js",
  "main": "index.js",
  "typings": "index.d.ts",
  "files": ["index.js", "index.d.ts"],
  "napi": {
    "name": "xxhash",
    "triples": {
      "defaults": true,
      "additional": [
        "i686-pc-windows-msvc",
        "x86_64-unknown-linux-musl",
        "aarch64-unknown-linux-gnu",
        "armv7-unknown-linux-gnueabihf",
        "aarch64-apple-darwin",
        "aarch64-linux-android",
        "armv7-linux-androideabi",
        "x86_64-unknown-freebsd",
        "aarch64-unknown-linux-musl",
        "aarch64-pc-windows-msvc"
      ]
    }
  },
  "engines": {"node": ">= 12"},
  "optionalDependencies": {
    "@node-rs/xxhash-win32-x64-msvc": "1.3.0",
    "@node-rs/xxhash-darwin-x64": "1.3.0",
    "@node-rs/xxhash-linux-x64-gnu": "1.3.0",
    "@node-rs/xxhash-win32-ia32-msvc": "1.3.0",
    "@node-rs/xxhash-linux-x64-musl": "1.3.0",
    "@node-rs/xxhash-linux-arm64-gnu": "1.3.0",
    "@node-rs/xxhash-linux-arm-gnueabihf": "1.3.0",
    "@node-rs/xxhash-darwin-arm64": "1.3.0",
    "@node-rs/xxhash-android-arm64": "1.3.0",
    "@node-rs/xxhash-android-arm-eabi": "1.3.0",
    "@node-rs/xxhash-freebsd-x64": "1.3.0",
    "@node-rs/xxhash-linux-arm64-musl": "1.3.0",
    "@node-rs/xxhash-win32-arm64-msvc": "1.3.0"
  }
}

Call order

Node and Rust calls rely on C ABI based FFI.

Node invokes plus100, which calls the FFI function __napi__plus_100, extracts arguments, calls the Rust plus_100, and returns the result as a napi_value.

Points to note

Package name changes

When renaming the root

package.json
name

, update all platform packages under npm to keep names in sync.

The napi.name field also influences generated .node filenames; adjust the require logic in index.js accordingly.

Rust enum vs. TS enum

Rust enums exported via NAPI‑RS lack the reverse‑mapping feature of TypeScript enums.

#[napi]
pub enum Kind {
  Duck,
  Dog,
  Cat,
}
{
  "Duck": 0,
  "Dog": 1,
  "Cat": 2
}

Object conversion cost

Getting or setting object properties incurs a round‑trip to Node and conversion between JS and Rust values. Prefer passing a struct instead of raw Object for performance.

#[napi(object)]
struct PackageJson {
  pub name: String,
  pub version: String,
  pub dependencies: Option<HashMap<String, String>>,
  pub dev_dependencies: Option<HashMap<String, String>>,
}

#[napi]
fn read_package_json(pkg_json: PackageJson) -> PackageJson { pkg_json }

Advanced usage

Enable BigInt support for u64, u128, i128 by adding features = ["napi6"] in Cargo.toml.

Types overwrite: use string arguments in the #[napi] macro to customize generated TypeScript signatures.

Async/Await: combine with tokio to return a Promise from Rust or accept a Promise from JS.

AsyncTask/Task: run CPU‑intensive work on libuv’s thread pool without blocking the event loop.

External: expose a native handle as a JS object with methods for further interaction.

#[napi(ts_args_type="callback: (err: null | Error, result: number) => void")]
fn call_threadsafe_function(callback: JsFunction) -> Result<()> {
  Ok(())
}

How to debug

Debug macros

VS Code’s Rust Analyzer provides built‑in debugging support.

Debug source

Use a standard VS Code JavaScript debug configuration or a JavaScript Debug Terminal to debug the JS side.

{
  "configurations": [
    {
      "name": "debug js",
      "type": "node",
      "request": "launch",
      "skipFiles": ["<node_internals>/**"],
      "cwd": "${workspaceFolder}",
      "program": "${file}"
    }
  ]
}

Debug Rust

Build a debug‑enabled artifact with napi build --platform and launch it via lldb -- /usr/local/bin/node app.js. VS Code tasks and launch configurations can automate this process.

{
  "version": "2.0.0",
  "tasks": [
    {
      "type": "npm",
      "script": "build:debug",
      "group": "build",
      "label": "npm: build:debug"
    }
  ]
}
{
  "configurations": [
    {
      "type": "lldb",
      "request": "launch",
      "sourceLanguages": ["rust"],
      "name": "debug rust",
      "program": "node",
      "preLaunchTask": "npm: build:debug",
      "args": ["--inspect", "${file}"]
    }
  ]
}
Cross‑platformRustNode.jsNAPI-RStype generationnative addons
Alipay Experience Technology
Written by

Alipay Experience Technology

Exploring ultimate user experience and best engineering practices

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.