# napi-rs: building Node.js add-ons in Rust without node-gyp

> napi-rs is a Rust framework for compiled Node.js add-ons that speak Node-API. It replaces the node-gyp toolchain with Cargo and the @napi-rs/cli, and it targets anyone who wants native speed inside a Node process without writing C++.

**napi-rs/napi-rs** — A framework for building compiled Node.js add-ons in Rust via Node-API

- Repository: https://github.com/napi-rs/napi-rs
- Website: https://napi.rs
- Stars: 7,958 · Forks: 413
- Language: Rust
- License: NOASSERTION
- Published: 2026-09-22 · Updated: 2026-09-22 · Language: en
- Canonical page: https://hysenlabs.com/projects/napi-rs-napi-rs

## The problem napi-rs solves: native code in Node without node-gyp

Node.js add-ons have traditionally meant C++ and node-gyp. That path pulls in Python, a system compiler, and a build configuration that behaves differently on every platform. napi-rs takes a different route: you write the add-on in Rust, annotate functions with a macro, and the toolchain produces a shared library that Node loads as a .node file. The README states that the crate allows you to build add-ons purely with the Rust/JavaScript toolchain and without involving node-gyp.

The audience is specific. If you already have Rust code (a parser, a codec, a numeric routine) and want to call it from JavaScript, napi-rs is the shortest path. If your native code is C or C++ and already works, the framework gives you little, because you would be rewriting the binding layer rather than reusing it. The project is also not a general FFI layer: it is bound to Node-API, and the README notes the library requires Node@10.0.0 or later.

## How the #[napi] macro and Node-API fit together

The mechanism is macro-driven code generation. You write ordinary Rust functions and mark them with #[napi]. The macro emits the registration and type-conversion glue that Node-API expects. The README's example defines a fibonacci function and states that module registration is done by the runtime, with no explicit registration step in user code.

Callbacks use Rust's Fn, FnMut or FnOnce traits, and the README notes that the return type of callbacks can only be Result. Async functions are supported when the async feature is enabled, as shown in the read_file_async example that returns a Buffer from tokio::fs::read. That is the data flow: JavaScript values cross into Node-API, the generated layer converts them into Rust types, your function runs, and the return value is converted back.

The build side is a Cargo crate. The README instructs you to set crate-type to "cdylib" so cargo builds a C-style shared library the Node executable can load, and to add a build.rs that calls napi_build::setup(). The README states that this build script has only been tested on macOS, Linux, Windows x64 MSVC and FreeBSD. That is a narrower statement than the platform support table, which lists many more targets including musl, Android, powerpc64le, s390x, loong64 and riscv64. The table describes release artifacts; the build script note describes where the build has actually been exercised. Treat the two as different claims.

## Install and first build with @napi-rs/cli

Start from a Cargo crate. The README's Cargo.toml snippet sets the crate type and the dependencies. Note the MSRV: the workspace declares rust-version = "1.88", and the README lists Rust 1.88.0 as the minimum supported version.

```toml
[package]
name = "awesome"

[lib]
crate-type = ["cdylib"]

[dependencies]
napi = "3"
napi-derive = "3"

[build-dependencies]
napi-build = "1"
```

Add the build script at the crate root. The README gives this exact file, and napi_build::setup() is what wires the Node-API build flags into the compilation.

```rust
// build.rs
extern crate napi_build;

fn main() {
  napi_build::setup();
}
```

Install the CLI as a local development dependency and declare the output name. The README shows a napi key with binaryName, plus build scripts that call napi build. The name in binaryName is what the generated artifact is called.

```json
{
  "name": "awesome-package",
  "devDependencies": {
    "@napi-rs/cli": "^3.0.0"
  },
  "napi": {
    "binaryName": "jarvis"
  },
  "scripts": {
    "build": "napi build --release",
    "build:debug": "napi build"
  }
}
```

After a build, require the generated file. The README states the module_name comes from your package name in Cargo.toml, with hyphens converted to underscores, so xxx becomes ./xxx.node and xxx-yyy becomes ./xxx_yyy.node.

```js
require('./jarvis.node')
```

You can also direct the output elsewhere. The README gives napi build [--release] ./dll and napi build [--release] ./artifacts as examples of copying the dynamic library to an appointed location.

## Testing a native add-on means testing from JavaScript

This is the part that surprises people coming from a pure Rust background. The README states that libraries depending on this crate must be loaded into a Node executable in order to resolve symbols, so all tests are written in JavaScript in the test_module subdirectory. The repository's own scripts follow that model: yarn build:test builds the example workspaces, and yarn test runs the workspace test suites.

There is a cargo test path too, exposed as test:macro, which runs cargo test -p napi-examples. But that is the repository's own macro test, not a general substitute for loading your add-on in Node. If your team expects cargo test to cover the binding surface, plan for a JavaScript test runner instead. The repository also carries separate memory-testing and bench workspaces, and CI workflows for Address Sanitizer and memory leak detection, which tells you the maintainers treat native memory behaviour as something to be measured rather than assumed.

## Where napi-rs is the wrong choice

The strongest limitation is the toolchain boundary. The README is explicit that the napi build script has only been tested on macOS, Linux, Windows x64 MSVC and FreeBSD. If you need to build from source on a target outside that list, you are on ground the documentation does not cover, even though prebuilt artifacts may exist for that platform. Cross-compilation and musl builds are where this bites hardest.

Second, the project is a framework, not a drop-in. Adopting it means your add-on is a Cargo crate with a cdylib crate type and a build script, and your build pipeline must run the CLI. That is a real migration for an existing C++ add-on, and it buys you nothing if the C++ code already works.

Third, the callback contract is restrictive by design. The README states that callback return types can only be Result. If your Rust API returns plain values and you want to pass closures across the boundary, you will be adapting signatures.

Finally, consider whether you need native code at all. The repository contains a wasm-runtime workspace, and the related searches show people comparing the two. A WebAssembly build avoids per-platform binaries and the cdylib toolchain entirely, at the cost of a different performance profile and different host integration. napi-rs is the right tool when you need direct access to the Node process, the filesystem, or native libraries that wasm cannot reach.

## napi-rs compared with Neon and node-bindgen

The README lists two related projects: neon and node-bindgen. The difference is in what the binding layer looks like. Neon is also Rust and also targets Node, but its model is a JavaScript-facing API built from Rust types with its own runtime abstractions; napi-rs instead leans on the #[napi] attribute macro and lets Node-API do the conversion, with the README describing registration as handled by the runtime. node-bindgen is the closer analogue in spirit, since it is also macro-based, but it is a separate project with its own conventions and its own support matrix. If you are choosing between them, the deciding factor is the platform table and the CLI: napi-rs ships @napi-rs/cli with the build, artifact naming and output-directory handling described above, and its support table covers musl, Android, FreeBSD, powerpc64le, s390x, loong64 and riscv64 in addition to the mainstream targets.

## Maintenance, licence and upgrade cost

The repository is not archived, and the last push was on 2026-09-21. Releases are cut per crate rather than as one monorepo version: napi-v3.12.7, napi-sys-v3.3.2 and napi-derive-v3.6.8 all landed on 2026-09-20. That versioning scheme matters for upgrades, because napi, napi-derive and napi-sys move independently, and the README's dependency examples pin napi and napi-derive at major version 3 while napi-build is at 1. A major bump in any one of them is a separate migration.

The licence situation needs care. The repository metadata reports NOASSERTION, while the package.json in the repository states "license": "MIT". Those two signals disagree, and the LICENSE file is the authoritative document. Read it before you redistribute binaries, and if your organisation has a policy gate, resolve the discrepancy rather than assuming MIT. This is not legal advice; it is a note that the two sources in this repository do not match.

Upgrade cost is mostly mechanical for patch releases, but the MSRV is a hard floor: Rust 1.88.0 per the README and rust-version = "1.88" in the workspace manifest. A toolchain older than that will not build the crates.

## Conclusion

Adopt napi-rs when you have Rust code that must run inside a Node process and you want Cargo plus @napi-rs/cli instead of node-gyp. Skip it if your native code is already C or C++ with an existing binding layer, or if a WebAssembly build meets your performance target, since the wasm-runtime workspace is a different trade-off. Before committing, verify three things against your own setup: that your Rust toolchain is at least 1.88.0, that crate-type = ["cdylib"] plus napi_build::setup() in build.rs produces a loadable artifact for every target triple you ship, and that your test story can live in JavaScript, because the README states that tests must be loaded into a Node executable to resolve symbols.

## FAQ

### What does napi-rs stand for?

The name combines Node-API, the stable ABI that Node.js exposes for native add-ons, with Rust, the language the framework targets. The README describes it as a framework for building compiled Node.js add-ons in Rust via Node-API.

### What is napi-rs?

It is a Rust framework for building compiled Node.js add-ons through Node-API. You annotate Rust functions with #[napi], set crate-type to "cdylib", and use @napi-rs/cli to build the crate and produce a loadable .node file.

### How does napi-rs compare with a WebAssembly approach?

The repository includes a wasm-runtime workspace alongside the native crates, so both exist under the same project. The README does not compare them directly; the difference is that napi-rs produces a platform-specific cdylib loaded by the Node executable, while a wasm build avoids per-platform native artifacts.

### What are the alternatives to napi-rs?

The README lists neon and node-bindgen as related projects. Both are Rust binding approaches, but napi-rs centres on the #[napi] attribute macro and ships @napi-rs/cli for building and placing the generated .node artifact.

## Sources

- [Issues](https://github.com/napi-rs/napi-rs/issues)
- [napi-rs/napi-rs on GitHub](https://github.com/napi-rs/napi-rs)
- [Project website](https://napi.rs)
- [README](https://github.com/napi-rs/napi-rs/blob/main/README.md)
- [Releases](https://github.com/napi-rs/napi-rs/releases)

---

Hysen Labs editorial analysis, written from the project's own repository and release notes. Cite the canonical page: https://hysenlabs.com/projects/napi-rs-napi-rs
