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.
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 sourceAutomatic 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_world → helloWorld). 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}"]
}
]
}Alipay Experience Technology
Exploring ultimate user experience and best engineering practices
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
