Backend Development 20 min read

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.

JD Retail Technology
JD Retail Technology
JD Retail Technology
Using NAPI‑RS to Develop, Debug, and Publish Node.js Extensions with Rust

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‑project

The 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-gnu

To 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); // 2

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

CI/CDBackend DevelopmentRustWebAssemblyNode.jsNAPI-RS
JD Retail Technology
Written by

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.

0 followers
Reader feedback

How this landed with the community

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