Manual setup

Contents

If you would rather wire up a project by hand, or you are adding @teakit/napi to a repo that already exists, this page is the complete path. It is a 1:1 of what napikit new writes, plus pointers into the publishing flow when you are ready to ship.

Use Zig 0.16.0 and Node.js >=20.19.0 for the napikit CLI. If the project uses Bun, the maintained baseline is Bun 1.3.14.

1. Add @teakit/napi and the napikit CLI

zig fetch --save git+https://github.com/teakit/napi.git/#HEAD
npm install -D napikit

The first command pins the napi Zig package in build.zig.zon. The second installs the napikit CLI as a dev dependency. Use pnpm, yarn, or bun if you prefer; the CLI is the same.

2. Write your addon

// src/lib.zig
const std = @import("std");
const napi = @import("napi");

comptime { napi.module(@This()); }

pub fn add(a: i32, b: i32) i32 {
    return a + b;
}

pub fn greet(env: napi.Env, name: []const u8) ![]const u8 {
    return std.fmt.allocPrint(env.allocator(), "Hello, {s}!", .{name});
}

napi.module(@This()) walks every public declaration of the file at comptime:

DeclarationBecomes
pub fn name(...)A JS function
pub const x = <JS-mappable value>A JS property
pub const x = struct { pub fn ... }A nested JS namespace

Names are translated snake_case to camelCase automatically. See Functions for the full rules.

3. Configure build.zig

const std = @import("std");
const napi = @import("napi");

pub fn build(b: *std.Build) void {
    const target = b.standardTargetOptions(.{});
    const optimize = b.standardOptimizeOption(.{});

    napi.addLib(b, b.dependency("napi", .{}), .{
        .name = "my-addon",
        .root = b.path("src/lib.zig"),
        .target = target,
        .optimize = optimize,
        .npm = .{
            .scope = "@myscope",
            .repository = "<owner>/<repo>",
        },
    });
}

addLib is the only thing you need to call. It registers an artifact, sets up the right linker flags for each OS, and (when --release is set) generates the cross-compile graph and the npm package skeleton. See build.zig (addLib) for every option.

The .scope you pick here is the npm scope your per-platform bindings publish under. Set it to a username or org you own, and create the org on npm before publishing. See Publishing: how distribution works for the full rationale.

Don't skip .repository. npm requires a repository field on every published package for provenance to verify against the source tree. Without it, napikit publish fails in CI with "package must specify a repository". An addon ships the main package plus one binding per platform (twelve package.json files with the default platform set), and setting .repository once here writes it into all of them on every release build, so you don't have to maintain twelve copies by hand. Use the "owner/repo" shorthand for GitHub or pass a full git URL. See addLib reference for the accepted forms.

4. Add scripts to package.json

{
  "name": "my-addon",
  "version": "0.0.0",
  "type": "module",
  "scripts": {
    "build": "napikit build",
    "release": "napikit build --release",
    "bump": "napikit bump",
    "test": "node test.mjs"
  },
  "devDependencies": {
    "napikit": "^0.1.0"
  }
}

The version field starts at 0.0.0. napikit bump manages it from here on.

5. Add a .gitignore

node_modules
zig-out
.zig-cache
zig-pkg
.DS_Store
*.tgz

# regenerated by `napikit build`
/my-addon.js
/my-addon.d.ts

Replace my-addon with your addon's .name. The two project-root files are re-exporters that napikit build writes so import addon from "./my-addon.js" works locally; they are derived artifacts and should not be committed. The npm/ directory is committed once you start publishing; it is the publishable tree, kept in sync with build.zig by every release build.

6. Build and use

napikit build
// test.mjs
import addon from "./my-addon.js";

console.log(addon.add(2, 3)); // 5
console.log(addon.greet("world")); // "Hello, world!"
node test.mjs

That is the full development loop. Edit src/lib.zig, re-run napikit build, re-run your test.

7. Set up publishing (when you are ready)

When you want to ship the addon to npm, you need two more pieces in place:

  • A CI workflow that publishes per-platform binaries on tag push. Save the YAML from Publishing: the CI workflow as .github/workflows/publish.yml.
  • The publish flow itself: cross-compile, push to GitHub, run napikit npm-init once, then napikit bump for every release.

Walk through Publishing to npm end to end. It assumes you reached it from this page or from napikit new; the steps are the same either way.

Next steps