<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>Forem: Daanyaal Sobani</title>
    <description>The latest articles on Forem by Daanyaal Sobani (@daanyaalsobani).</description>
    <link>https://forem.com/daanyaalsobani</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F2409993%2Facc881c1-3b0e-4166-8b1e-8b60cbd8d56a.jpeg</url>
      <title>Forem: Daanyaal Sobani</title>
      <link>https://forem.com/daanyaalsobani</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://forem.com/feed/daanyaalsobani"/>
    <language>en</language>
    <item>
      <title>Calling Rust from Node.js: A Practical Guide to NAPI-RS</title>
      <dc:creator>Daanyaal Sobani</dc:creator>
      <pubDate>Sun, 19 Apr 2026 23:52:18 +0000</pubDate>
      <link>https://forem.com/daanyaalsobani/calling-rust-from-nodejs-a-practical-guide-to-napi-rs-47om</link>
      <guid>https://forem.com/daanyaalsobani/calling-rust-from-nodejs-a-practical-guide-to-napi-rs-47om</guid>
      <description>&lt;h3&gt;
  
  
  Note: I used Claude to write this whole article based on my github and youtube video linked at the end.
&lt;/h3&gt;

&lt;p&gt;You have a Node.js backend. Somewhere in it, there's a hot path — maybe image processing, maybe a parser, maybe a prime-checker for some cursed reason — and you'd like it to be faster. You've heard Rust is fast. But you don't want to rewrite your whole backend in Rust, and you &lt;em&gt;really&lt;/em&gt; don't want to spin up a separate Rust web server just to expose one function over HTTP.&lt;/p&gt;

&lt;p&gt;Enter &lt;strong&gt;NAPI-RS&lt;/strong&gt;: a framework for compiling Rust code into a native Node.js addon that you can &lt;code&gt;require()&lt;/code&gt; like any other module. No IPC. No sockets. No serialization. Your Rust function becomes a JavaScript function, living in the same process, called with a direct function pointer jump.&lt;/p&gt;

&lt;p&gt;This post walks through building a minimal demo end-to-end, then unpacks what's actually going on at the ABI / dynamic-linking level. If you prefer video, there's a &lt;a href="https://www.youtube.com/watch?v=aZdy_hkxyas" rel="noopener noreferrer"&gt;YouTube walkthrough&lt;/a&gt; and the &lt;a href="https://github.com/DaanyaalSobani/NAPI-RS-DEMO" rel="noopener noreferrer"&gt;full code on GitHub&lt;/a&gt;.&lt;/p&gt;

&lt;h2&gt;
  
  
  What we're building
&lt;/h2&gt;

&lt;p&gt;A tiny Express server with two routes, both backed by Rust:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;GET /plus100/:n&lt;/code&gt; — returns &lt;code&gt;n + 100&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;GET /is-prime/:n&lt;/code&gt; — returns whether &lt;code&gt;n&lt;/code&gt; is prime&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Nothing fancy. The point is the integration pattern, not the algorithm.&lt;/p&gt;

&lt;h2&gt;
  
  
  Prerequisites
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Node.js 18+&lt;/strong&gt; (&lt;a href="https://nodejs.org" rel="noopener noreferrer"&gt;nodejs.org&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Rust&lt;/strong&gt; via &lt;a href="https://rustup.rs" rel="noopener noreferrer"&gt;rustup&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;A C toolchain&lt;/strong&gt; for linking:

&lt;ul&gt;
&lt;li&gt;Windows: &lt;a href="https://visualstudio.microsoft.com/visual-cpp-build-tools/" rel="noopener noreferrer"&gt;Visual Studio Build Tools&lt;/a&gt; with "Desktop development with C++"&lt;/li&gt;
&lt;li&gt;macOS: &lt;code&gt;xcode-select --install&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;Linux: &lt;code&gt;sudo apt install build-essential&lt;/code&gt; (or your distro's equivalent)&lt;/li&gt;
&lt;/ul&gt;


&lt;/li&gt;

&lt;/ul&gt;

&lt;p&gt;Sanity check:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;node &lt;span class="nt"&gt;--version&lt;/span&gt;
cargo &lt;span class="nt"&gt;--version&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;If those both print versions, you're ready.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 1: Install the NAPI-RS CLI
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; &lt;span class="nt"&gt;-g&lt;/span&gt; @napi-rs/cli
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You could also use &lt;code&gt;npx @napi-rs/cli&lt;/code&gt; each time, but installing globally gives you the shorter &lt;code&gt;napi&lt;/code&gt; command.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 2: Create the outer Node project
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;mkdir &lt;/span&gt;my-app &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;my-app
npm init &lt;span class="nt"&gt;-y&lt;/span&gt;
npm &lt;span class="nb"&gt;install &lt;/span&gt;express
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Standard Express setup. Nothing unusual so far.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 3: Scaffold the Rust addon
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;napi new rust_functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;It'll ask a few questions. Reasonable answers:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Package name:&lt;/strong&gt; &lt;code&gt;rust_functions&lt;/code&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Minimum Node-API version:&lt;/strong&gt; &lt;code&gt;napi9&lt;/code&gt; (default — works on Node 18.17+)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Target:&lt;/strong&gt; pick your current platform (e.g., &lt;code&gt;x86_64-pc-windows-msvc&lt;/code&gt; on Windows, &lt;code&gt;aarch64-apple-darwin&lt;/code&gt; on Apple Silicon)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;GitHub Actions:&lt;/strong&gt; no (keeps things simple)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;License:&lt;/strong&gt; MIT&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;This creates a &lt;code&gt;rust_functions/&lt;/code&gt; subfolder with a full scaffold — &lt;code&gt;Cargo.toml&lt;/code&gt;, &lt;code&gt;src/lib.rs&lt;/code&gt;, its own &lt;code&gt;package.json&lt;/code&gt;, a build script, and some npm glue.&lt;/p&gt;

&lt;p&gt;Install its dependencies:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;&lt;span class="nb"&gt;cd &lt;/span&gt;rust_functions
npm &lt;span class="nb"&gt;install
cd&lt;/span&gt; ..
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The layout now looks like this:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight plaintext"&gt;&lt;code&gt;my-app/
├── package.json
└── rust_functions/
    ├── Cargo.toml
    ├── src/lib.rs        ← your Rust code goes here
    ├── package.json
    ├── build.rs
    └── ...
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;blockquote&gt;
&lt;p&gt;&lt;strong&gt;Note on the target prompt:&lt;/strong&gt; the target you pick during scaffolding &lt;em&gt;only&lt;/em&gt; affects what CI would prebuild if you published to npm. It doesn't restrict who can run &lt;code&gt;napi build&lt;/code&gt; locally. A teammate on macOS can clone this project and &lt;code&gt;npm run build&lt;/code&gt; without any changes — napi auto-detects their platform.&lt;/p&gt;
&lt;/blockquote&gt;

&lt;h2&gt;
  
  
  Step 4: Write the Rust
&lt;/h2&gt;

&lt;p&gt;Open &lt;code&gt;rust_functions/src/lib.rs&lt;/code&gt; and replace it with:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#![deny(clippy::all)]&lt;/span&gt;

&lt;span class="k"&gt;use&lt;/span&gt; &lt;span class="nn"&gt;napi_derive&lt;/span&gt;&lt;span class="p"&gt;::&lt;/span&gt;&lt;span class="n"&gt;napi&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

&lt;span class="nd"&gt;#[napi(js_name&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"plus100"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;plus_100&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="nd"&gt;#[napi(js_name&lt;/span&gt; &lt;span class="nd"&gt;=&lt;/span&gt; &lt;span class="s"&gt;"isPrime"&lt;/span&gt;&lt;span class="nd"&gt;)]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;is_prime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;n&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;bool&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;4&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;true&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="k"&gt;mut&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="mi"&gt;3&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="k"&gt;while&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;*&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;=&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;if&lt;/span&gt; &lt;span class="n"&gt;n&lt;/span&gt; &lt;span class="o"&gt;%&lt;/span&gt; &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;==&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="k"&gt;false&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;
    &lt;span class="n"&gt;i&lt;/span&gt; &lt;span class="o"&gt;+=&lt;/span&gt; &lt;span class="mi"&gt;2&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="k"&gt;true&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;A few things worth calling out:&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;The &lt;code&gt;#[napi]&lt;/code&gt; macro is the whole magic.&lt;/strong&gt; It wraps your function in the FFI glue Node needs to call it — we'll dig into that later. Without the macro, this is just plain Rust.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;&lt;code&gt;js_name&lt;/code&gt; controls the exported name.&lt;/strong&gt; By default, NAPI-RS converts &lt;code&gt;snake_case&lt;/code&gt; → &lt;code&gt;camelCase&lt;/code&gt;, so &lt;code&gt;is_prime&lt;/code&gt; would naturally become &lt;code&gt;isPrime&lt;/code&gt;. But names with numbers can convert in surprising ways (e.g., &lt;code&gt;plus_100&lt;/code&gt; loses its underscore and becomes &lt;code&gt;plus100&lt;/code&gt;), so setting &lt;code&gt;js_name&lt;/code&gt; explicitly is the safest move.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;u32&lt;/code&gt; and not &lt;code&gt;u64&lt;/code&gt;?&lt;/strong&gt; NAPI-RS doesn't auto-convert &lt;code&gt;u64&lt;/code&gt; because it doesn't fit cleanly into JavaScript's native &lt;code&gt;number&lt;/code&gt; or &lt;code&gt;BigInt&lt;/code&gt; types. For this demo, &lt;code&gt;u32&lt;/code&gt; (max ~4.3 billion) is more than enough. If you need larger ranges, use &lt;code&gt;i64&lt;/code&gt; (which maps to &lt;code&gt;BigInt&lt;/code&gt; on the JS side).&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Why &lt;code&gt;mut i&lt;/code&gt;?&lt;/strong&gt; Rust variables are immutable by default. Since we're reassigning &lt;code&gt;i += 2&lt;/code&gt; each loop iteration, we have to opt into mutability with &lt;code&gt;mut&lt;/code&gt;. Forgetting this is the #1 early-Rust error.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 5: Link the addon into the outer app
&lt;/h2&gt;

&lt;p&gt;From the outer &lt;code&gt;my-app/&lt;/code&gt; folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm &lt;span class="nb"&gt;install&lt;/span&gt; ./rust_functions
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This doesn't copy anything. It creates a symlink at &lt;code&gt;node_modules/rust_functions&lt;/code&gt; pointing back to your &lt;code&gt;rust_functions/&lt;/code&gt; folder, and adds an entry in your outer &lt;code&gt;package.json&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="nl"&gt;"dependencies"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"rust_functions"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"file:rust_functions"&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The symlink means that whenever you rebuild the Rust addon, the outer app sees the new binary immediately. No &lt;code&gt;npm install&lt;/code&gt; needed on every change.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 6: Add build scripts
&lt;/h2&gt;

&lt;p&gt;Edit the outer &lt;code&gt;package.json&lt;/code&gt; and add these scripts:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight json"&gt;&lt;code&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="nl"&gt;"scripts"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="p"&gt;{&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"start"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"node server.js"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"build:rust"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"cd rust_functions &amp;amp;&amp;amp; npm run build"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;&lt;span class="w"&gt;
    &lt;/span&gt;&lt;span class="nl"&gt;"dev"&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt;&lt;span class="w"&gt; &lt;/span&gt;&lt;span class="s2"&gt;"npm run build:rust &amp;amp;&amp;amp; node server.js"&lt;/span&gt;&lt;span class="w"&gt;
  &lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;span class="p"&gt;}&lt;/span&gt;&lt;span class="w"&gt;
&lt;/span&gt;&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Now from the outer folder:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run build:rust
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;You'll see cargo compile things, and when it finishes, there'll be a new file in &lt;code&gt;rust_functions/&lt;/code&gt; with a name like:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;rust_functions.win32-x64-msvc.node&lt;/code&gt; (Windows)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rust_functions.darwin-arm64.node&lt;/code&gt; (Apple Silicon Mac)&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;rust_functions.linux-x64-gnu.node&lt;/code&gt; (Linux x64)&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;&lt;strong&gt;That file is your compiled Rust.&lt;/strong&gt; It's literally a DLL on Windows, a dylib on macOS, an &lt;code&gt;.so&lt;/code&gt; on Linux — just with a &lt;code&gt;.node&lt;/code&gt; extension so Node knows to treat it as a native addon.&lt;/p&gt;

&lt;h2&gt;
  
  
  Step 7: Write the server
&lt;/h2&gt;

&lt;p&gt;Create &lt;code&gt;server.js&lt;/code&gt;:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;express&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;express&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;plus100&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;isPrime&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;rust_functions&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;express&lt;/span&gt;&lt;span class="p"&gt;();&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/plus100/:n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;n must be a number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;result&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;plus100&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;/is-prime/:n&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;req&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;params&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;if &lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;!&lt;/span&gt;&lt;span class="nb"&gt;Number&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;isFinite&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;||&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt; &lt;span class="o"&gt;&amp;lt;&lt;/span&gt; &lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
    &lt;span class="k"&gt;return&lt;/span&gt; &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;status&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;400&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;error&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;n must be a non-negative number&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="nx"&gt;res&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;json&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nf"&gt;isPrime&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
&lt;span class="p"&gt;});&lt;/span&gt;

&lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;listen&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;3000&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;console&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;log&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;listening on 3000&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;));&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Two things to notice:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;The destructuring with &lt;code&gt;{ }&lt;/code&gt; matters.&lt;/strong&gt; &lt;code&gt;const { plus100 } = require(...)&lt;/code&gt; pulls the function off the module's exports object. Without the braces, you'd get the whole module object and &lt;code&gt;plus100(5)&lt;/code&gt; would fail with "not a function." Easy mistake to make.&lt;/p&gt;&lt;/li&gt;
&lt;li&gt;&lt;p&gt;&lt;strong&gt;There's no indication these are Rust functions.&lt;/strong&gt; &lt;code&gt;plus100&lt;/code&gt; and &lt;code&gt;isPrime&lt;/code&gt; 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.&lt;/p&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;h2&gt;
  
  
  Step 8: Run it
&lt;/h2&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm start
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;In another terminal:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;curl http://localhost:3000/plus100/42
&lt;span class="c"&gt;# {"input":42,"result":142}&lt;/span&gt;

curl http://localhost:3000/is-prime/1000000007
&lt;span class="c"&gt;# {"n":1000000007,"isPrime":true}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. You're calling Rust from Express.&lt;/p&gt;

&lt;h2&gt;
  
  
  The development loop
&lt;/h2&gt;

&lt;p&gt;The magic moment — edit &lt;code&gt;rust_functions/src/lib.rs&lt;/code&gt;, add a new function, then:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;npm run dev
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This rebuilds the Rust addon and restarts the server in one command. Change Rust → rebuild → hit the new endpoint. The iteration cycle is surprisingly fast: cargo does incremental compilation, and NAPI's build step just wraps it.&lt;/p&gt;




&lt;p&gt;Now for the part most tutorials skip: &lt;strong&gt;what is actually happening&lt;/strong&gt;?&lt;/p&gt;

&lt;h2&gt;
  
  
  What NAPI-RS is doing, under the hood
&lt;/h2&gt;

&lt;h3&gt;
  
  
  Node-API (N-API): the stable C ABI
&lt;/h3&gt;

&lt;p&gt;Node.js has always had a way to run native code — things like &lt;code&gt;fs&lt;/code&gt;, &lt;code&gt;crypto&lt;/code&gt;, and &lt;code&gt;zlib&lt;/code&gt; aren't written in JavaScript. But historically, writing your &lt;em&gt;own&lt;/em&gt; native addon meant targeting V8 directly (V8 is the JavaScript engine inside Node). V8's internals change between versions, so every Node release broke every native addon. It was painful.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;Node-API (N-API)&lt;/strong&gt; fixed this by exposing a &lt;em&gt;stable C ABI&lt;/em&gt; for native addons. An addon compiled against N-API version 9 keeps working on every future Node version that supports N-API 9 or higher — no rebuild needed. That stability is the whole reason this approach is usable.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;ABI&lt;/strong&gt; = Application Binary Interface. It's the binary-level contract between two pieces of compiled code: how arguments are passed in registers, how structs are laid out in memory, how symbols are named. Two binaries can only call each other if they agree on ABI. Node-API is that agreement, written down and frozen.&lt;/p&gt;

&lt;p&gt;Why C specifically? Because C has a simple, well-defined ABI that every systems language can speak. Rust can produce code with C ABI (via &lt;code&gt;extern "C"&lt;/code&gt; and &lt;code&gt;#[repr(C)]&lt;/code&gt;). So can Go, Zig, even plain assembly. By exposing a C interface, Node makes itself reachable from basically any language that can compile to native code.&lt;/p&gt;

&lt;h3&gt;
  
  
  NAPI-RS: the Rust wrapper
&lt;/h3&gt;

&lt;p&gt;N-API is a C API. Writing against it directly from Rust is verbose, unsafe FFI code — raw pointers, manual type conversions, manual error handling. &lt;strong&gt;NAPI-RS is a Rust framework that generates all that glue code for you.&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;When you write:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="nd"&gt;#[napi]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;plus_100&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;The macro expands (roughly) into:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight rust"&gt;&lt;code&gt;&lt;span class="c1"&gt;// Your original function, unchanged&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;plus_100&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;100&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Generated C-ABI wrapper&lt;/span&gt;
&lt;span class="nd"&gt;#[no_mangle]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;__napi_wrapper_plus_100&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;napi_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;napi_callback_info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;napi_value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="c1"&gt;// 1. Extract arguments from JS&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;napi_get_cb_info&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;info&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="o"&gt;...&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="nb"&gt;u32&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;napi_get_value_uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;args&lt;/span&gt;&lt;span class="p"&gt;[&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;]);&lt;/span&gt;

  &lt;span class="c1"&gt;// 2. Call your actual Rust function&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;plus_100&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;input&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="c1"&gt;// 3. Convert the result back to a JS value&lt;/span&gt;
  &lt;span class="nf"&gt;napi_create_uint32&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;result&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;

&lt;span class="c1"&gt;// Registration entry: called when Node loads the .node file&lt;/span&gt;
&lt;span class="nd"&gt;#[no_mangle]&lt;/span&gt;
&lt;span class="k"&gt;pub&lt;/span&gt; &lt;span class="k"&gt;extern&lt;/span&gt; &lt;span class="s"&gt;"C"&lt;/span&gt; &lt;span class="k"&gt;fn&lt;/span&gt; &lt;span class="nf"&gt;napi_register_module_v1&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
  &lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;napi_env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
  &lt;span class="n"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="n"&gt;napi_value&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt;
&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="k"&gt;-&amp;gt;&lt;/span&gt; &lt;span class="n"&gt;napi_value&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
  &lt;span class="k"&gt;let&lt;/span&gt; &lt;span class="n"&gt;fn_value&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;napi_create_function&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"plus100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;__napi_wrapper_plus_100&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="nf"&gt;napi_set_named_property&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="n"&gt;env&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;exports&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="s"&gt;"plus100"&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="n"&gt;fn_value&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
  &lt;span class="n"&gt;exports&lt;/span&gt;
&lt;span class="p"&gt;}&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's the FFI boilerplate you'd otherwise write by hand. The &lt;code&gt;#[napi]&lt;/code&gt; macro generates it so you don't have to.&lt;/p&gt;

&lt;h3&gt;
  
  
  The lifecycle of a &lt;code&gt;require('rust_functions')&lt;/code&gt;
&lt;/h3&gt;

&lt;p&gt;When your server does &lt;code&gt;require('rust_functions')&lt;/code&gt;, here's what happens:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;
&lt;strong&gt;Node resolves the module&lt;/strong&gt; to &lt;code&gt;rust_functions/index.js&lt;/code&gt; (because &lt;code&gt;package.json&lt;/code&gt; says &lt;code&gt;"main": "index.js"&lt;/code&gt;).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;&lt;code&gt;index.js&lt;/code&gt; is auto-generated by napi&lt;/strong&gt; and contains platform-detection logic. It inspects &lt;code&gt;process.platform&lt;/code&gt; and &lt;code&gt;process.arch&lt;/code&gt;, then does &lt;code&gt;require('./rust_functions.win32-x64-msvc.node')&lt;/code&gt; (or whichever binary matches the current machine).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node sees the &lt;code&gt;.node&lt;/code&gt; extension&lt;/strong&gt; and invokes its native module loader. On Windows this calls &lt;code&gt;LoadLibraryEx()&lt;/code&gt;, on Linux/macOS it calls &lt;code&gt;dlopen()&lt;/code&gt;. Same syscalls a C program would use.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The OS loads the DLL into the Node process's memory space.&lt;/strong&gt; Your Rust code, compiled to machine instructions, now lives in Node's address space.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Node looks up the symbol &lt;code&gt;napi_register_module_v1&lt;/code&gt;&lt;/strong&gt; (via &lt;code&gt;GetProcAddress&lt;/code&gt; on Windows, &lt;code&gt;dlsym&lt;/code&gt; on Unix) and calls it.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;The registration function runs&lt;/strong&gt;, creates JS function objects backed by your C wrappers, attaches them to an exports object.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;That exports object is what &lt;code&gt;require()&lt;/code&gt; returns.&lt;/strong&gt; Your JS sees &lt;code&gt;{ plus100: [Function], isPrime: [Function] }&lt;/code&gt;.&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;From there, calling &lt;code&gt;plus100(5)&lt;/code&gt; is a direct function call. V8 invokes the wrapper's C function pointer, the wrapper converts &lt;code&gt;5&lt;/code&gt; to a Rust &lt;code&gt;u32&lt;/code&gt;, your Rust runs, the wrapper converts &lt;code&gt;105&lt;/code&gt; back to a JS number, done. All in the same process. No IPC, no serialization.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why this is so fast
&lt;/h3&gt;

&lt;p&gt;Most "call X from Y" solutions involve crossing a process boundary. Let's compare, roughly, for a trivial function call:&lt;/p&gt;

&lt;div class="table-wrapper-paragraph"&gt;&lt;table&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th&gt;Approach&lt;/th&gt;
&lt;th&gt;Per-call cost&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td&gt;
&lt;strong&gt;NAPI-RS addon&lt;/strong&gt; (same process)&lt;/td&gt;
&lt;td&gt;~100–500 ns&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Long-lived Rust process + JSON over pipes&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~50–200 μs&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Localhost HTTP sidecar&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;~500 μs – 2 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;tr&gt;
&lt;td&gt;&lt;strong&gt;Spawning a fresh Rust subprocess each call&lt;/strong&gt;&lt;/td&gt;
&lt;td&gt;1–50 ms&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;&lt;/div&gt;

&lt;p&gt;That's often a 1000× difference or more. The reason is that everything besides NAPI involves some combination of: kernel-mediated IPC, serializing data into bytes, deserializing on the other side, possibly starting up whole new processes. NAPI skips all of it by collapsing Rust and Node into a single process. The "boundary" between languages becomes just a couple of type conversions on a function call.&lt;/p&gt;

&lt;p&gt;This is the whole point. If you only need cross-language integration occasionally or for coarse-grained batch work, a subprocess or HTTP sidecar is fine. But if you need to call Rust in a hot loop — say, processing every request, or in a tight inner loop — NAPI is usually the only approach that doesn't get destroyed by overhead.&lt;/p&gt;

&lt;h3&gt;
  
  
  The tradeoff
&lt;/h3&gt;

&lt;p&gt;NAPI isn't free. In exchange for speed, you accept:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Coupling:&lt;/strong&gt; if your Rust panics and isn't caught, it can crash the entire Node process. Subprocesses isolate crashes; NAPI doesn't.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Platform-specific binaries:&lt;/strong&gt; your &lt;code&gt;.node&lt;/code&gt; 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).&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;FFI conversion costs:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Build complexity:&lt;/strong&gt; you now need a C toolchain and Rust installed to build the project.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;For performance-critical code paths, these are usually acceptable. For "I want to use one Rust library once a day in a cron job," spawning a subprocess is simpler.&lt;/p&gt;

&lt;h3&gt;
  
  
  Why there are no name collisions
&lt;/h3&gt;

&lt;p&gt;A natural question: what if two different NAPI addons both export a function named &lt;code&gt;compute&lt;/code&gt;?&lt;/p&gt;

&lt;p&gt;No collision. Each &lt;code&gt;.node&lt;/code&gt; file is loaded via &lt;code&gt;dlopen&lt;/code&gt; with its own handle, into its own symbol namespace. Node calls each addon's &lt;code&gt;napi_register_module_v1&lt;/code&gt; separately, and each builds its own exports object. In JS:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight javascript"&gt;&lt;code&gt;&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;a&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addon-a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// { compute: [Function], ... }&lt;/span&gt;
&lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;b&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;require&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;addon-b&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// { compute: [Function], ... }&lt;/span&gt;
&lt;span class="nx"&gt;a&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// calls addon-a's Rust&lt;/span&gt;
&lt;span class="nx"&gt;b&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;compute&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;5&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// calls addon-b's Rust&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Each is a property on a different object. The namespacing is exactly the same as for regular JS modules — because at the &lt;code&gt;require()&lt;/code&gt; level, there's no difference.&lt;/p&gt;

&lt;h3&gt;
  
  
  The big reframe
&lt;/h3&gt;

&lt;p&gt;The model most people start with is: "Rust is a separate program; I'd have to shell out to it." NAPI flips this. &lt;strong&gt;Rust code becomes part of your Node process.&lt;/strong&gt; It's not a tool you're invoking, it's a library you've linked. Same process, same memory, same lifetime.&lt;/p&gt;

&lt;p&gt;This is dynamic linking, and it's the same technology operating systems have supported since the 1980s. &lt;code&gt;require('./foo.node')&lt;/code&gt; is basically just a JavaScript-friendly wrapper around &lt;code&gt;LoadLibrary("foo.dll")&lt;/code&gt;. The novelty isn't the mechanism — it's that NAPI-RS makes it ergonomic enough that you can write normal idiomatic Rust and have it show up as normal idiomatic JavaScript.&lt;/p&gt;

&lt;h2&gt;
  
  
  When to reach for NAPI-RS
&lt;/h2&gt;

&lt;p&gt;Good fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Hot-path computation:&lt;/strong&gt; parsing, hashing, compression, image processing, pathfinding — anything called frequently where Rust's speed pays off.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Wrapping existing Rust libraries:&lt;/strong&gt; if someone's already written a great Rust crate for what you need, NAPI-RS exposes it to Node cheaply.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Portable performance wins within a Node codebase:&lt;/strong&gt; you get Rust speed without restructuring your application.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;Less good fits:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;One-off CLI-style tools:&lt;/strong&gt; &lt;code&gt;child_process.spawn()&lt;/code&gt; is simpler.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Services with independent scaling or deployment needs:&lt;/strong&gt; a separate HTTP service is more flexible.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;I/O-bound workloads:&lt;/strong&gt; Rust's advantage is CPU; for I/O-bound Node code, the event loop and fast I/O libraries already handle things well.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  What to explore next
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;Async Rust functions:&lt;/strong&gt; NAPI-RS supports &lt;code&gt;async fn&lt;/code&gt; with &lt;code&gt;#[napi]&lt;/code&gt; — calls return JS Promises and run on a worker thread so they don't block Node's event loop.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Passing structs:&lt;/strong&gt; &lt;code&gt;#[napi(object)]&lt;/code&gt; on a Rust struct lets you pass it directly to/from JS as a plain object, with automatic field conversion.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Publishing to npm:&lt;/strong&gt; the &lt;code&gt;napi prepublish&lt;/code&gt; workflow handles packaging prebuilt binaries for multiple platforms so your users don't need Rust installed.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;WebAssembly as an alternative:&lt;/strong&gt; 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.&lt;/li&gt;
&lt;/ul&gt;

&lt;h2&gt;
  
  
  Resources
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;🎥 &lt;a href="https://www.youtube.com/watch?v=aZdy_hkxyas" rel="noopener noreferrer"&gt;YouTube walkthrough of this tutorial&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;💻 &lt;a href="https://github.com/DaanyaalSobani/NAPI-RS-DEMO" rel="noopener noreferrer"&gt;Full code on GitHub&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 &lt;a href="https://napi.rs" rel="noopener noreferrer"&gt;NAPI-RS documentation&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;📚 &lt;a href="https://nodejs.org/api/n-api.html" rel="noopener noreferrer"&gt;Node-API reference&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;




&lt;p&gt;If you found this useful, drop a comment below — I write occasional deep dives on systems-adjacent topics when something catches my eye. Next up I'm thinking about digging into WebAssembly as an alternative path, or how Node's event loop actually works beyond the meme diagram. Let me know what you'd want to read.&lt;/p&gt;

</description>
      <category>rust</category>
      <category>node</category>
      <category>webdev</category>
      <category>tutorial</category>
    </item>
  </channel>
</rss>
