A Zig N-API wrapper library and CLI for building and publishing cross-platform Node.js native addons.
zapi provides two main components:
- Zig Library (
src/) - Idiomatic Zig bindings for the Node.js N-API, making it easy to write native addons in Zig - CLI Tool (
ts/) - Build tooling for cross-compiling and publishing multi-platform npm packages
npm install -D @chainsafe/zapiAdd the Zig dependency to your build.zig.zon:
.dependencies = .{
.zapi = .{
.url = "https://github.com/chainsafe/zapi/archive/<commit>.tar.gz",
.hash = "...",
},
},const napi = @import("napi");
comptime {
napi.module.register(initModule);
}
fn initModule(env: napi.Env, module: napi.Value) !void {
// Export a string
try module.setNamedProperty("greeting", try env.createStringUtf8("Hello from Zig!"));
// Export a function
try module.setNamedProperty("add", try env.createFunction("add", 2, napi.createCallback(2, add, .{}), null));
}
fn add(a: i32, b: i32) i32 {
return a + b;
}| Type | Description |
|---|---|
Env |
The N-API environment, provides methods to create values, throw errors, manage scopes |
Value |
A JavaScript value handle with methods for type checking, property access, conversions |
CallbackInfo |
Provides access to function arguments and this binding |
HandleScope |
Prevents garbage collection of values within a scope |
EscapableHandleScope |
Like HandleScope but allows one value to escape |
Ref |
A persistent reference to a value that survives garbage collection |
Deferred |
Resolver/rejecter for promises |
AsyncWork |
Run work on a thread pool with completion callback on main thread |
ThreadSafeFunction |
Call JavaScript from any thread safely |
AsyncContext |
Context for async resource tracking |
Full control using raw Env and Value:
fn add_manual(env: napi.Env, info: napi.CallbackInfo(2)) !napi.Value {
const a = try info.arg(0).getValueInt32();
const b = try info.arg(1).getValueInt32();
return try env.createInt32(a + b);
}Let zapi handle argument/return conversion:
// Arguments and return value are automatically converted
fn add(a: i32, b: i32) i32 {
return a + b;
}
// Register with automatic wrapping
try env.createFunction("add", 2, napi.createCallback(2, add, .{}), null);Control how arguments are converted:
napi.createCallback(2, myFunc, .{
.args = .{ .env, .auto, .value, .data, .string, .buffer },
.returns = .value, // or .string, .buffer, .auto
});| Hint | Description |
|---|---|
.auto |
Automatic type conversion |
.env |
Inject napi.Env |
.value |
Pass raw napi.Value |
.data |
User data pointer passed to createFunction |
.string |
Convert to/from []const u8 |
.buffer |
Convert to/from byte slice |
const Timer = struct {
start: i64,
pub fn read(self: *Timer) i64 {
return std.time.milliTimestamp() - self.start;
}
};
try env.defineClass(
"Timer",
0,
timerConstructor,
null,
&[_]napi.c.napi_property_descriptor{
.{ .utf8name = "read", .method = napi.wrapCallback(0, Timer.read) },
},
);Run CPU-intensive work off the main thread:
const Work = struct {
a: i32,
b: i32,
result: i32,
deferred: napi.Deferred,
};
fn execute(env: napi.Env, data: *Work) void {
// Runs on thread pool - don't call JS here!
data.result = data.a + data.b;
}
fn complete(env: napi.Env, status: napi.status.Status, data: *Work) void {
// Back on main thread - resolve the promise
const result = env.createInt32(data.result) catch return;
data.deferred.resolve(result) catch return;
}
// Create async work
const work = try napi.AsyncWork(Work).create(env, null, name, execute, complete, &data);
try work.queue();Call JavaScript from any thread:
const tsfn = try env.createThreadsafeFunction(
jsCallback, // JS function to call
context, // User context
"name",
0, // Max queue size (0 = unlimited)
1, // Initial thread count
null, // Finalize data
null, // Finalize callback
myCallJsCallback, // Called on main thread
);
// From any thread:
try tsfn.call(&data, .blocking);All N-API calls return NapiError on failure:
fn myFunction(env: napi.Env) !void {
// Errors propagate naturally
const value = try env.createStringUtf8("hello");
// Throw JavaScript errors
try env.throwError("ERR_CODE", "Something went wrong");
try env.throwTypeError("ERR_TYPE", "Expected a number");
}Add a zapi field to your package.json:
{
"name": "my-addon",
"zapi": {
"binaryName": "my-addon",
"step": "my-lib",
"targets": [
"x86_64-unknown-linux-gnu",
"x86_64-unknown-linux-musl",
"aarch64-unknown-linux-gnu",
"x86_64-apple-darwin",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc"
]
}
}| Target | Platform | Arch | ABI |
|---|---|---|---|
aarch64-apple-darwin |
macOS | arm64 | - |
x86_64-apple-darwin |
macOS | x64 | - |
aarch64-unknown-linux-gnu |
Linux | arm64 | glibc |
x86_64-unknown-linux-gnu |
Linux | x64 | glibc |
x86_64-unknown-linux-musl |
Linux | x64 | musl |
x86_64-pc-windows-msvc |
Windows | x64 | msvc |
| Option | Description |
|---|---|
--help, -h |
Show help message |
--version, -v |
Show version number |
Build for a single target platform.
zapi build [options]| Option | Description | Default |
|---|---|---|
--step |
Zig build step | zapi.step from package.json |
--target |
Target triple | Current platform |
--optimize |
Debug, ReleaseSafe, ReleaseFast, ReleaseSmall |
- |
--zig-cwd |
Working directory for zig build | . |
Build for all configured targets and collect artifacts.
zapi build-artifacts [options]| Option | Description | Default |
|---|---|---|
--step |
Zig build step | zapi.step from package.json |
--optimize |
Optimization level | - |
--zig-cwd |
Working directory for zig build | . |
--artifacts-dir |
Output directory for artifacts | artifacts |
Example output:
▶ Building my-addon for 6 target(s)...
[1/6] Building for x86_64-unknown-linux-gnu...
→ Moving artifact to artifacts/x86_64-unknown-linux-gnu
[2/6] Building for aarch64-apple-darwin...
→ Moving artifact to artifacts/aarch64-apple-darwin
...
✓ Built 6 artifact(s) to artifacts/
Prepare npm packages for publishing:
- Creates
npm/<target>/directories for each target - Moves compiled
.nodebinaries from artifacts into target packages - Generates
package.jsonfor each target package (with correctos,cpu,libc) - Updates the main
package.jsonwithoptionalDependencies
zapi prepublish [options]| Option | Description | Default |
|---|---|---|
--artifacts-dir |
Directory containing built artifacts | artifacts |
--npm-dir |
Directory for npm packages | npm |
Example output:
▶ Preparing [email protected] for publishing...
▶ Moving artifacts to npm packages...
→ x86_64-unknown-linux-gnu → npm/x86_64-unknown-linux-gnu/my-addon.node
▶ Generating target package.json files...
→ Created npm/x86_64-unknown-linux-gnu/package.json
▶ Updating package.json with optionalDependencies...
✓ Prepared 6 target package(s) in npm/
Publish all target-specific packages and the main package to npm.
zapi publish [options] [-- <npm-args>]| Option | Description | Default |
|---|---|---|
--npm-dir |
Directory containing npm packages | npm |
--dry-run |
Preview what would be published without publishing | false |
Any arguments after -- are passed directly to npm publish (e.g., --access public, --tag beta).
Example dry-run:
zapi publish --dry-run▶ [DRY RUN] Would publish 6 target package(s) + main package
→ Extra npm args: (none)
[1/7] Would publish x86_64-unknown-linux-gnu
→ Directory: /path/to/npm/x86_64-unknown-linux-gnu
...
✓ [DRY RUN] 7 package(s) would be published
# 1. Build for all targets
zapi build-artifacts --optimize ReleaseFast
# 2. Prepare npm packages
zapi prepublish
# 3. Preview what will be published
zapi publish --dry-run
# 4. Publish to npm
zapi publish -- --access publicSet DEBUG=1 for full stack traces on errors.
Load the native addon, automatically selecting the correct binary for the current platform:
import { requireNapiLibrary } from "@chainsafe/zapi";
import { fileURLToPath } from "node:url";
import { dirname } from "node:path";
const __dirname = dirname(fileURLToPath(import.meta.url));
const addon = requireNapiLibrary(__dirname);Resolution order:
- Local build:
zig-out/lib/<binaryName>.node - Published package:
<pkg-name>-<target>
See the example/ directory for a comprehensive example including:
- String properties
- Functions with manual and automatic argument handling
- Classes with methods
- Async work with promises
- Thread-safe functions
# Build the example
zig build
# Test it
node example/test.jsMIT