node --version
cargo --version
node --version
cargo --version
node --version
cargo --version
npm install -g @napi-rs/cli
npm install -g @napi-rs/cli
npm install -g @napi-rs/cli
mkdir my-app && cd my-app
npm init -y
npm install express
mkdir my-app && cd my-app
npm init -y
npm install express
mkdir my-app && cd my-app
npm init -y
npm install express
napi new rust_functions
napi new rust_functions
napi new rust_functions
cd rust_functions
npm install
cd ..
cd rust_functions
npm install
cd ..
cd rust_functions
npm install
cd ..
my-app/
├── package.json
└── rust_functions/ ├── Cargo.toml ├── src/lib.rs ← your Rust code goes here ├── package.json ├── build.rs └── ...
my-app/
├── package.json
└── rust_functions/ ├── Cargo.toml ├── src/lib.rs ← your Rust code goes here ├── package.json ├── build.rs └── ...
my-app/
├── package.json
└── rust_functions/ ├── Cargo.toml ├── src/lib.rs ← your Rust code goes here ├── package.json ├── build.rs └── ...
#![deny(clippy::all)] use napi_derive::napi; #[napi(js_name = "plus100")]
pub fn plus_100(input: u32) -> u32 { input + 100
} #[napi(js_name = "isPrime")]
pub fn is_prime(n: u32) -> bool { if n < 2 { return false; } if n < 4 { return true; } if n % 2 == 0 { return false; } let mut i: u32 = 3; while i * i <= n { if n % i == 0 { return false; } i += 2; } true
}
#![deny(clippy::all)] use napi_derive::napi; #[napi(js_name = "plus100")]
pub fn plus_100(input: u32) -> u32 { input + 100
} #[napi(js_name = "isPrime")]
pub fn is_prime(n: u32) -> bool { if n < 2 { return false; } if n < 4 { return true; } if n % 2 == 0 { return false; } let mut i: u32 = 3; while i * i <= n { if n % i == 0 { return false; } i += 2; } true
}
#![deny(clippy::all)] use napi_derive::napi; #[napi(js_name = "plus100")]
pub fn plus_100(input: u32) -> u32 { input + 100
} #[napi(js_name = "isPrime")]
pub fn is_prime(n: u32) -> bool { if n < 2 { return false; } if n < 4 { return true; } if n % 2 == 0 { return false; } let mut i: u32 = 3; while i * i <= n { if n % i == 0 { return false; } i += 2; } true
}
npm install ./rust_functions
npm install ./rust_functions
npm install ./rust_functions
"dependencies": { "rust_functions": "file:rust_functions"
}
"dependencies": { "rust_functions": "file:rust_functions"
}
"dependencies": { "rust_functions": "file:rust_functions"
}
{ "scripts": { "start": "node server.js", "build:rust": "cd rust_functions && npm run build", "dev": "npm run build:rust && node server.js" }
}
{ "scripts": { "start": "node server.js", "build:rust": "cd rust_functions && npm run build", "dev": "npm run build:rust && node server.js" }
}
{ "scripts": { "start": "node server.js", "build:rust": "cd rust_functions && npm run build", "dev": "npm run build:rust && node server.js" }
}
npm run build:rust
npm run build:rust
npm run build:rust
const express = require('express');
const { plus100, isPrime } = require('rust_functions'); const app = express(); app.get('/plus100/:n', (req, res) => { const n = Number(req.params.n); if (!Number.isFinite(n)) { return res.status(400).json({ error: 'n must be a number' }); } res.json({ input: n, result: plus100(n) });
}); app.get('/is-prime/:n', (req, res) => { const n = Number(req.params.n); if (!Number.isFinite(n) || n < 0) { return res.status(400).json({ error: 'n must be a non-negative number' }); } res.json({ n, isPrime: isPrime(n) });
}); app.listen(3000, () => console.log('listening on 3000'));
const express = require('express');
const { plus100, isPrime } = require('rust_functions'); const app = express(); app.get('/plus100/:n', (req, res) => { const n = Number(req.params.n); if (!Number.isFinite(n)) { return res.status(400).json({ error: 'n must be a number' }); } res.json({ input: n, result: plus100(n) });
}); app.get('/is-prime/:n', (req, res) => { const n = Number(req.params.n); if (!Number.isFinite(n) || n < 0) { return res.status(400).json({ error: 'n must be a non-negative number' }); } res.json({ n, isPrime: isPrime(n) });
}); app.listen(3000, () => console.log('listening on 3000'));
const express = require('express');
const { plus100, isPrime } = require('rust_functions'); const app = express(); app.get('/plus100/:n', (req, res) => { const n = Number(req.params.n); if (!Number.isFinite(n)) { return res.status(400).json({ error: 'n must be a number' }); } res.json({ input: n, result: plus100(n) });
}); app.get('/is-prime/:n', (req, res) => { const n = Number(req.params.n); if (!Number.isFinite(n) || n < 0) { return res.status(400).json({ error: 'n must be a non-negative number' }); } res.json({ n, isPrime: isPrime(n) });
}); app.listen(3000, () => console.log('listening on 3000'));
curl http://localhost:3000/plus100/42
# {"input":42,"result":142} curl http://localhost:3000/is-prime/1000000007
# {"n":1000000007,"isPrime":true}
curl http://localhost:3000/plus100/42
# {"input":42,"result":142} curl http://localhost:3000/is-prime/1000000007
# {"n":1000000007,"isPrime":true}
curl http://localhost:3000/plus100/42
# {"input":42,"result":142} curl http://localhost:3000/is-prime/1000000007
# {"n":1000000007,"isPrime":true}
npm run dev
npm run dev
npm run dev
#[napi]
pub fn plus_100(input: u32) -> u32 { input + 100
}
#[napi]
pub fn plus_100(input: u32) -> u32 { input + 100
}
#[napi]
pub fn plus_100(input: u32) -> u32 { input + 100
}
// Your original function, unchanged
pub fn plus_100(input: u32) -> u32 { input + 100 } // Generated C-ABI wrapper
#[no_mangle]
pub extern "C" fn __napi_wrapper_plus_100( env: napi_env, info: napi_callback_info,
) -> napi_value { // 1. Extract arguments from JS let args = napi_get_cb_info(env, info, ...); let input: u32 = napi_get_value_uint32(env, args[0]); // 2. Call your actual Rust function let result = plus_100(input); // 3. Convert the result back to a JS value napi_create_uint32(env, result)
} // Registration entry: called when Node loads the .node file
#[no_mangle]
pub extern "C" fn napi_register_module_v1( env: napi_env, exports: napi_value,
) -> napi_value { let fn_value = napi_create_function(env, "plus100", __napi_wrapper_plus_100); napi_set_named_property(env, exports, "plus100", fn_value); exports
}
// Your original function, unchanged
pub fn plus_100(input: u32) -> u32 { input + 100 } // Generated C-ABI wrapper
#[no_mangle]
pub extern "C" fn __napi_wrapper_plus_100( env: napi_env, info: napi_callback_info,
) -> napi_value { // 1. Extract arguments from JS let args = napi_get_cb_info(env, info, ...); let input: u32 = napi_get_value_uint32(env, args[0]); // 2. Call your actual Rust function let result = plus_100(input); // 3. Convert the result back to a JS value napi_create_uint32(env, result)
} // Registration entry: called when Node loads the .node file
#[no_mangle]
pub extern "C" fn napi_register_module_v1( env: napi_env, exports: napi_value,
) -> napi_value { let fn_value = napi_create_function(env, "plus100", __napi_wrapper_plus_100); napi_set_named_property(env, exports, "plus100", fn_value); exports
}
// Your original function, unchanged
pub fn plus_100(input: u32) -> u32 { input + 100 } // Generated C-ABI wrapper
#[no_mangle]
pub extern "C" fn __napi_wrapper_plus_100( env: napi_env, info: napi_callback_info,
) -> napi_value { // 1. Extract arguments from JS let args = napi_get_cb_info(env, info, ...); let input: u32 = napi_get_value_uint32(env, args[0]); // 2. Call your actual Rust function let result = plus_100(input); // 3. Convert the result back to a JS value napi_create_uint32(env, result)
} // Registration entry: called when Node loads the .node file
#[no_mangle]
pub extern "C" fn napi_register_module_v1( env: napi_env, exports: napi_value,
) -> napi_value { let fn_value = napi_create_function(env, "plus100", __napi_wrapper_plus_100); napi_set_named_property(env, exports, "plus100", fn_value); exports
}
const a = require('addon-a'); // { compute: [Function], ... }
const b = require('addon-b'); // { compute: [Function], ... }
a.compute(5); // calls addon-a's Rust
b.compute(5); // calls addon-b's Rust
const a = require('addon-a'); // { compute: [Function], ... }
const b = require('addon-b'); // { compute: [Function], ... }
a.compute(5); // calls addon-a's Rust
b.compute(5); // calls addon-b's Rust
const a = require('addon-a'); // { compute: [Function], ... }
const b = require('addon-b'); // { compute: [Function], ... }
a.compute(5); // calls addon-a's Rust
b.compute(5); // calls addon-b's Rust - GET /plus100/:n — returns n + 100
- GET /is-prime/:n — returns whether n is prime - Node.js 18+ (nodejs.org)
- Rust via rustup
- A C toolchain for linking: Windows: Visual Studio Build Tools with "Desktop development with C++"
macOS: xcode-select --install Linux: sudo apt install build-essential (or your distro's equivalent)
- Windows: Visual Studio Build Tools with "Desktop development with C++"
- macOS: xcode-select --install
- Linux: sudo apt install build-essential (or your distro's equivalent) - Windows: Visual Studio Build Tools with "Desktop development with C++"
- macOS: xcode-select --install
- Linux: sudo apt install build-essential (or your distro's equivalent) - Package name: rust_functions
- Minimum Node-API version: napi9 (default — works on Node 18.17+)
- Target: pick your current platform (e.g., x86_64-pc-windows-msvc on Windows, aarch64-apple-darwin on Apple Silicon)
- GitHub Actions: no (keeps things simple)
- License: MIT - rust_functions.win32-x64-msvc.node (Windows)
- rust_functions.darwin-arm64.node (Apple Silicon Mac)
- rust_functions.linux-x64-gnu.node (Linux x64) - The destructuring with { } matters. const { plus100 } = require(...) pulls the function off the module's exports object. Without the braces, you'd get the whole module object and plus100(5) would fail with "not a function." Easy mistake to make.
- There's no indication these are Rust functions. plus100 and isPrime are just regular JavaScript functions as far as your code is concerned. The whole point of NAPI-RS is that the language boundary disappears once you've crossed it. - Node resolves the module to rust_functions/index.js (because package.json says "main": "index.js").
- index.js is auto-generated by napi and contains platform-detection logic. It inspects process.platform and process.arch, then does require('./rust_functions.win32-x64-msvc.node') (or whichever binary matches the current machine).
- Node sees the .node extension and invokes its native module loader. On Windows this calls LoadLibraryEx(), on Linux/macOS it calls dlopen(). Same syscalls a C program would use.
- The OS loads the DLL into the Node process's memory space. Your Rust code, compiled to machine instructions, now lives in Node's address space.
- Node looks up the symbol napi_register_module_v1 (via GetProcAddress on Windows, dlsym on Unix) and calls it.
- The registration function runs, creates JS function objects backed by your C wrappers, attaches them to an exports object.
- That exports object is what require() returns. Your JS sees { plus100: [Function], isPrime: [Function] }. - Coupling: if your Rust panics and isn't caught, it can crash the entire Node process. Subprocesses isolate crashes; NAPI doesn't.
- Platform-specific binaries: your .node file only runs on the OS and architecture it was compiled for. Each user rebuilds locally (or you ship multiple prebuilt binaries for different platforms).
- FFI conversion costs: simple primitives are cheap, but passing large structured data (big strings, nested objects) incurs serialization-like costs in the conversion layer. Not as bad as JSON over a pipe, but not free.
- Build complexity: you now need a C toolchain and Rust installed to build the project. - Hot-path computation: parsing, hashing, compression, image processing, pathfinding — anything called frequently where Rust's speed pays off.
- Wrapping existing Rust libraries: if someone's already written a great Rust crate for what you need, NAPI-RS exposes it to Node cheaply.
- Portable performance wins within a Node codebase: you get Rust speed without restructuring your application. - One-off CLI-style tools: child_process.spawn() is simpler.
- Services with independent scaling or deployment needs: a separate HTTP service is more flexible.
- I/O-bound workloads: Rust's advantage is CPU; for I/O-bound Node code, the event loop and fast I/O libraries already handle things well. - Async Rust functions: NAPI-RS supports async fn with #[napi] — calls return JS Promises and run on a worker thread so they don't block Node's event loop.
- Passing structs: #[napi(object)] on a Rust struct lets you pass it directly to/from JS as a plain object, with automatic field conversion.
- Publishing to npm: the napi prepublish workflow handles packaging prebuilt binaries for multiple platforms so your users don't need Rust installed.
- WebAssembly as an alternative: if you need portability (same binary on every platform) or sandboxing (untrusted code, plugins), WASM trades a bit of performance and access for those properties. - 🎥 YouTube walkthrough of this tutorial
- 💻 Full code on GitHub
- 📚 NAPI-RS documentation
- 📚 Node-API reference