Back to BlogNode.js

Node.js 26.1 Adds node:ffi: How to Call Native Libraries Without a Node-API Addon

Node.js 26.1 quietly shipped an experimental `node:ffi` module. It lets JavaScript call native libraries directly, which is exciting, useful, and a little terrifying. Here is what it does well, where it breaks down, and the first kind of binding I would actually build with it.

nodejsffinative modulesjavascriptruntime
Node.js 26.1 Adds node:ffi: How to Call Native Libraries Without a Node-API Addon

Why this landed harder than I expected

Most Node 26 coverage this week went straight to the obvious stuff in the 26.0.0 release: Temporal is on by default, V8 moved to 14.6, and Node 26 started its six month run as the Current release before LTS in October.

Fair enough. But the release I keep coming back to is Node 26.1.0, because it quietly added an experimental node:ffi module. If you have ever built a tiny native addon just to call one C function, you immediately get why this is interesting.

node:ffi gives Node a built-in foreign function interface. No third-party binding layer first. No Node-API wrapper first. You can load a shared library, describe a function signature, and call it from JavaScript.

That is useful. It is also the sort of feature that can absolutely ruin your afternoon if you get cocky.

What node:ffi actually gives you

The new FFI docs are refreshingly blunt: this API is unsafe, experimental, and capable of crashing your process or corrupting memory if you pass a bad pointer or declare the wrong signature. Good. It should say that.

Under the hood, node:ffi gives you two big buckets of tools:

  • Dynamic library loading with dlopen(), symbol lookup, and callable JS wrappers

  • Raw memory helpers for strings, buffers, array buffers, and pointers

A few details matter right away.

First, it is gated behind --experimental-ffi, and it only exists under the node: scheme, so you import it as node:ffi. If you use Node's permission model, you also need --allow-ffi.

Second, bundled libffi support is not universal. The docs currently list macOS and Windows on x64 and arm64, plus FreeBSD and Linux on arm, arm64, and x64. If you live outside that list, this is not plug-and-play yet.

Third, the API has a couple of small but very real footguns. My favorite one because it is so easy to miss: bool is marshaled as an 8-bit unsigned integer, and the docs explicitly say to pass 0 or 1. Not true or false. That is the kind of detail that tells you this feature is powerful, but not here to hold your hand.

The first demo worth trying

I would not start with callbacks or raw pointer juggling. That is how you end up reading crash logs instead of shipping code. Start with one tiny shared library and one dead simple function.

// add.c
int add_i32(int a, int b) {
  return a + b;
}

Compile that however you normally build a shared library for your platform. Then call it from Node like this:

import { dlopen, suffix } from 'node:ffi';

{
  using handle = dlopen(`./libadd.${suffix}`, {
    add_i32: {
      parameters: ['i32', 'i32'],
      result: 'i32',
    },
  });

  console.log(handle.functions.add_i32(20, 22));
}

Run it with:

node --experimental-ffi demo.mjs

That using block is not just nice syntax. It matters. The docs say resolved wrappers become invalid after the library is closed, and callback pointers become dangerous if native code still holds them after cleanup. In other words, lifetime management is the whole game here.

The small win I like most is suffix. It gives you the right shared library extension for the current platform, so your demo code does not immediately turn into a pile of if (process.platform === ...) checks.

Where this is better than a full addon

This is the part that makes node:ffi genuinely interesting.

If you need to call a stable C ABI from internal tooling, a migration script, a build helper, or a one-purpose service, node:ffi can be a much better fit than spinning up a full addon project. Sometimes you do not need a native module. You need one bridge.

A few good fits jump out:

  • Calling a tiny internal shared library that your company already ships

  • Prototyping a binding before committing to a real Node-API addon

  • Wrapping a handful of mature C functions with boring signatures like integers, strings, and buffers

  • Building tooling where the deployment environment is tightly controlled

That last point matters more than people admit. Experimental native bridges are much less scary when you own the machines, the library version, and the runtime version.

There is also a maintenance angle here. A lot of native bindings do not fail because the core idea was wrong. They fail because the setup got annoying. Build flags drift. CI images change. One platform breaks. If node:ffi lets you skip that whole layer for the simple cases, that is a real improvement.

Where I would back away slowly

I would not treat this as a general replacement for Node-API addons. Not yet.

I also would not build an npm package around it today unless the package is very explicit about requiring Node 26 Current and accepting experimental runtime behavior. That is a tiny compatibility window. Most teams still run LTS for good reasons.

The docs wave a lot of red flags, and they are not being dramatic. A few stand out:

  • Wrong signatures can crash the process

  • Zero-copy memory views can become invalid if the native side frees memory

  • Callback pointers can become dangerous after library.close() or unregistering

  • Closing a library from an active callback is explicitly unsupported

That is before you get to the practical stuff. Debugging a bad FFI boundary is usually worse than debugging JavaScript and worse than debugging a well-structured addon. You get the worst kind of bug: something that looks random until it does not.

So my rule is simple. If the boundary is small, stable, and easy to reason about, node:ffi looks great. If the native side has tricky ownership rules, complex structs, thread-sensitive callbacks, or a habit of evolving under you, I would rather pay the upfront Node-API cost.

My take

node:ffi is the most interesting thing in Node 26.1 by a mile.

Not because everyone should use it. Most people should not. But because it opens a very specific door that used to require more ceremony than the job deserved. For thin native bridges, internal tools, and experiments, this could save a lot of pointless boilerplate.

I would still keep it out of boring production services for now. It is experimental, it ships on the Current line, and the docs basically beg you to respect memory ownership like an adult.

Still, this is exactly the kind of feature I want in Node core: sharp, practical, and honest about the risks. If you are already on Node 26, this is worth trying on a toy library this week. You will know pretty quickly whether you found a new escape hatch or a new way to segfault your app.