Using NAPI‑RS to Develop, Debug, and Publish Node.js Extensions with Rust
This article introduces how to use NAPI‑RS for developing, debugging, and publishing Node.js extensions in Rust, covering Rust’s growing role in frontend tooling, project setup with @napi‑rs/cli, exposing functions and objects to JavaScript, handling callbacks, asynchronous calls, and CI/CD build and release processes.
Rust is increasingly popular in many domains, including operating‑system kernels, graphics, game development, and edge computing. Its performance and safety make it an attractive choice for front‑end build tools, where projects such as Turbopack, Parcel, Rspack, and Farm are already written in Rust.
When integrating Rust with Node.js there are two main approaches: compile Rust to WebAssembly (WASM) via wasm‑pack , or build native Node addons using NAPI‑RS or Neon. The article recommends the addon approach with napi‑rs because it provides a simple, lightweight API and does not require recompilation for different Node versions.
Project initialization
@napi-rs/cli init my‑projectThe generated project contains a package.json that defines platform‑specific binary packages and an index.js that loads the correct binary at runtime.
{
"name": "@tarojs/parse-css-to-stylesheet-darwin-x64",
"version": "0.0.25",
"os": ["darwin"],
"cpu": ["x64"],
"main": "parse-css-to-stylesheet.darwin-x64.node",
"files": ["parse-css-to-stylesheet.darwin-x64.node"],
"license": "MIT",
"engines": {"node": ">= 10"},
"repository": "https://github.com/NervJS/parse-css-to-stylesheet"
}The main entry loads the appropriate binary based on the host platform:
switch (platform) {
case 'win32':
switch (arch) {
case 'x64':
// load win32‑x64‑msvc binary
}
break;
// other platforms …
}Supported triples are listed in @napi‑rs/triples . For most projects only four platforms are needed:
x86_64-apple-darwin
aarch64-apple-darwin
x86_64-pc-windows-msvc
x86_64-unknown-linux-gnuTo add Apple Silicon support, extend the napi field in package.json :
"napi": {
"binaryName": "taro",
"triples": {
"default": true,
"additional": ["aarch64-apple-darwin"]
}
}Exposing Rust functions to JavaScript
// src/lib.rs
use napi_derive::napi;
#[napi]
pub fn plus_100(input: u32) -> u32 {
input + 100
}The generated TypeScript definition looks like:
export function plus100(input: number): number;Attributes such as js_name can rename the exported function, and more complex types (constants, objects, classes, enums) can also be exposed.
Passing objects from JavaScript to Rust
// Rust struct exposed as a JavaScript object
#[napi(object)]
pub struct Project {
pub project_root: String,
pub project_name: String,
pub npm: NpmType,
pub description: Option
,
pub typescript: Option
,
pub template: String,
pub css: CSSType,
pub auto_install: Option
,
pub framework: FrameworkType,
pub template_root: String,
pub version: String,
pub date: Option
,
pub compiler: Option
,
pub period: PeriodType,
}JavaScript can then call:
export function createProject(conf: Project) { /* … */ }Calling JavaScript from Rust
Rust can receive a ThreadsafeFunction and invoke a JavaScript callback. The basic call looks like:
#[napi]
pub fn call_threadsafe_function(callback: ThreadsafeFunction
) -> Result<()> {
for n in 0..100 {
let tsfn = callback.clone();
thread::spawn(move || {
tsfn.call(Ok(n), ThreadsafeFunctionCallMode::Blocking);
});
}
Ok(())
}To obtain a return value, use call_with_return_value or the async call_async API (requires the tokio_rt feature). Example of call_async :
#[cfg(feature = "tokio_rt")]
pub async fn call_async
(
&self,
value: Result
) -> Result
{
let (sender, receiver) = tokio::sync::oneshot::channel::
>();
self.handle.with_read_aborted(|aborted| {
if aborted { return Err(crate::Error::from_status(Status::Closing)); }
unsafe {
sys::napi_call_threadsafe_function(
self.handle.get_raw(),
Box::into_raw(Box::new(value.map(|data| {
ThreadsafeFunctionCallJsBackData {
data,
call_variant: ThreadsafeFunctionCallVariant::WithCallback,
callback: Box::new(move |d| {
sender.send(d.and_then(|d| D::from_napi_value(d.env, d.value)))
.map_err(|_| crate::Error::from_reason("Failed to send return value"))
})
}
}))),
ThreadsafeFunctionCallMode::NonBlocking.into(),
)
}
})?;
receiver.await.map_err(|_| crate::Error::new(Status::GenericFailure, "Receive value failed",))?.and_then(|ret| ret)
}JavaScript receives an async function:
export function callThreadsafeFunction(callback: (err: Error | null, value: number) => any): Promise
;Example usage:
const result = await callThreadsafeFunction((err, value) => value + 1);
console.log(result); // 2Debugging with VSCode
{
"version": "0.2.0",
"configurations": [
{
"type": "lldb",
"request": "launch",
"name": "debug-init",
"sourceLanguages": ["rust"],
"program": "node",
"args": [
"${workspaceFolder}/packages/taro-cli/bin/taro",
"init",
"test_pro"
],
"cwd": "${workspaceFolder}",
"preLaunchTask": "build binding debug",
"postDebugTask": "remove test_pro"
}
]
}Set breakpoints in the Rust source, launch the configuration, and VSCode will attach to the Node process running the compiled addon.
CI/CD build and release
The default @napi‑rs/cli template uses GitHub Actions to build binaries for the four platforms, upload them as artifacts, run tests, and publish to npm when a semantic version tag is pushed.
$ git commit -m '0.0.1'Build scripts can be customized to skip unnecessary platforms, disable JavaScript glue generation ( napi build --no-js ), or move artifacts to a different directory ( napi artifacts --npm-dir ../../npm2 --cwd ./ ).
Conclusion
Rust’s performance, safety, and modern language features make it a powerful tool for front‑end tooling and Node.js extensions. By using NAPI‑RS developers can write high‑performance native code, expose rich APIs to JavaScript, handle asynchronous callbacks efficiently, and integrate seamlessly into existing CI pipelines.
JD Retail Technology
Official platform of JD Retail Technology, delivering insightful R&D news and a deep look into the lives and work of technologists.
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.