---
title: "Zig e Node.js: Addons Nativos com N-API sem Sofrimento"
url: "https://ziglang.com.br/artigos/zig-componentes-nativos-nodejs-napi/"
markdown_url: "https://ziglang.com.br/artigos/zig-componentes-nativos-nodejs-napi.MD"
description: "Como usar Zig para criar componentes nativos para Node.js: quando faz sentido, arquitetura com N-API, build, memória, erros, empacotamento e cuidados de produção."
date: "2026-05-26"
author: ""
---

# Zig e Node.js: Addons Nativos com N-API sem Sofrimento

Como usar Zig para criar componentes nativos para Node.js: quando faz sentido, arquitetura com N-API, build, memória, erros, empacotamento e cuidados de produção.


Node.js resolve muito bem APIs, filas, automações, painéis internos e produtos SaaS. O problema aparece quando uma parte específica do sistema deixa de ser “código de negócio” e vira processamento pesado: parsing de arquivos grandes, compressão, validação binária, cálculo numérico, normalização de logs, criptografia, manipulação de imagens, varredura de diretórios ou integração com uma biblioteca C existente.

A saída comum é reescrever tudo em outra linguagem. Quase sempre é exagero. Uma alternativa mais pragmática é manter a aplicação em Node.js e mover apenas o núcleo quente para um addon nativo. Zig entra bem nesse espaço porque gera bibliotecas pequenas, conversa com C sem cerimônia, deixa alocação explícita e facilita cross-compilation. Você continua usando TypeScript, npm, Fastify, BullMQ ou Next.js no restante da aplicação, mas ganha um trecho nativo onde o gargalo realmente mora.

Este guia mostra quando um addon Node.js em Zig faz sentido, como desenhar uma fronteira segura com N-API, quais cuidados tomar com memória e erros, e como empacotar isso sem transformar o projeto em uma coleção de scripts frágeis.

## Quando vale a pena usar Zig com Node.js

Use Zig para um componente Node.js quando pelo menos uma destas condições for verdadeira:

- a função processa buffers grandes muitas vezes por segundo;
- o código precisa chamar uma biblioteca C ou uma ABI já existente;
- a latência p95/p99 importa mais do que a velocidade média;
- o módulo precisa rodar sem instalar Python, gcc ou toolchain no servidor final;
- o trabalho é determinístico e bem isolado, como parser, codec, hash, filtro ou validador;
- você quer entregar binários pequenos para Linux, macOS e Windows.

Não use Zig para substituir regra de negócio comum. Se a função faz três consultas HTTP, lê uma tabela no banco e decide qual e-mail enviar, Node.js continua sendo a camada certa. Zig deve entrar como bisturi, não como martelo.

Também vale comparar com outras opções. Para extensões nativas em Python, veja [Zig para extensões Python de alta performance](/artigos/zig-python-extensoes-nativas-performance/). Para módulos WebAssembly, leia [Zig e WebAssembly em 2026](/artigos/zig-webassembly-2026/). Para CLIs internas que rodam fora do processo Node, comece por [ferramentas internas em Zig para DevOps](/artigos/zig-ferramentas-internas-devops/).

## N-API em uma frase

N-API é a interface C estável do Node.js para addons nativos. Em vez de depender dos detalhes internos do V8, o addon fala com uma ABI mais estável: recebe `napi_env`, lê argumentos JavaScript, cria valores de retorno e registra funções exportadas.

Para Zig, isso é atraente porque a linguagem importa headers C diretamente com `@cImport`. O projeto pode incluir `node_api.h`, compilar uma biblioteca dinâmica e exportar uma função de inicialização que o Node carrega com `require()` ou `import` via um wrapper JavaScript.

O desenho mental fica assim:

```text
TypeScript/JavaScript
  └─ wrapper pequeno: valida API pública e carrega binário certo
       └─ addon .node compilado com Zig
            ├─ ponte N-API: converte JS <-> Zig
            └─ núcleo Zig: parser, algoritmo, codec ou integração C
```

A fronteira N-API deve ser fina. Quanto menos lógica de produto atravessar essa camada, mais simples fica testar, versionar e debugar.

## Estrutura de projeto

Uma estrutura simples para começar:

```text
meu-addon/
  package.json
  src/
    index.ts
    loader.ts
  native/
    build.zig
    src/
      addon.zig
      core.zig
  test/
    addon.test.ts
```

O `loader.ts` escolhe o binário certo para a plataforma atual. O `addon.zig` conhece N-API. O `core.zig` não conhece Node.js; ele recebe bytes e retorna um resultado Zig normal. Essa separação permite testar o núcleo com `zig build test` sem iniciar Node.

Um wrapper TypeScript pode ter esta forma:

```ts
import { existsSync } from "node:fs";
import { join } from "node:path";

const platform = `${process.platform}-${process.arch}`;
const candidates = [
  join(__dirname, `../prebuilds/${platform}/meu_addon.node`),
  join(__dirname, "../native/zig-out/lib/meu_addon.node"),
];

const nativePath = candidates.find(existsSync);
if (!nativePath) {
  throw new Error(`Binário nativo não encontrado para ${platform}`);
}

export const native = require(nativePath) as {
  checksum(input: Buffer): number;
};
```

O wrapper é também o lugar para mensagens de erro humanas. Não deixe o usuário final encarar `ERR_DLOPEN_FAILED` sem contexto.

## Núcleo Zig sem dependência de Node

Antes de tocar em N-API, escreva e teste o núcleo:

```zig
const std = @import("std");

test "checksum simples" {
    try std.testing.expectEqual(@as(u32, 294), checksum("abc"));
}

pub fn checksum(input: []const u8) u32 {
    var acc: u32 = 0;
    for (input) |byte| {
        acc +%= byte;
    }
    return acc;
}
```

Este exemplo é trivial, mas a regra é importante: parsing, validação, cálculo e acesso a bibliotecas C devem morar em funções Zig comuns. A ponte com JavaScript só transforma valores.

Para processamento maior, prefira APIs que aceitam `[]const u8` e escrevem em um buffer de saída fornecido pelo chamador. Isso reduz cópias e deixa a posse de memória explícita.

## Ponte N-API mínima

O arquivo de ponte importa `node_api.h` e expõe funções no formato esperado pelo Node:

```zig
const std = @import("std");
const core = @import("core.zig");
const c = @cImport({
    @cInclude("node_api.h");
});

fn jsChecksum(env: c.napi_env, info: c.napi_callback_info) callconv(.C) c.napi_value {
    var argc: usize = 1;
    var args: [1]c.napi_value = undefined;
    _ = c.napi_get_cb_info(env, info, &argc, &args, null, null);

    var data: ?*anyopaque = null;
    var len: usize = 0;
    if (argc != 1 or c.napi_get_buffer_info(env, args[0], &data, &len) != c.napi_ok) {
        return throwTypeError(env, "checksum espera um Buffer");
    }

    const bytes = @as([*]const u8, @ptrCast(data.?))[0..len];
    const value = core.checksum(bytes);

    var result: c.napi_value = undefined;
    _ = c.napi_create_uint32(env, value, &result);
    return result;
}
```

O exemplo omite detalhes de registro para manter o foco. Em produção, trate todo status de N-API. Se uma chamada retornar algo diferente de `napi_ok`, converta para uma exceção JavaScript ou retorne `null` somente quando isso estiver documentado.

## Memória: a regra mais importante

A maior fonte de bugs em addons nativos é posse de memória. A regra prática é simples: **não retorne ponteiro para memória Zig temporária como se fosse memória JavaScript permanente**.

Caminhos seguros:

- se o retorno é pequeno, crie string ou buffer JavaScript com N-API e copie;
- se o retorno é grande, considere `napi_create_external_buffer` com finalizer claro;
- se a função recebe `Buffer`, trate o ponteiro como emprestado e válido só durante a chamada;
- se guardar estado entre chamadas, exponha um handle opaco com finalizer;
- se usar allocator, deixe o dono explícito e teste vazamentos.

Um padrão útil é manter o núcleo Zig sem alocar quando possível:

```zig
pub fn encode(input: []const u8, output: []u8) !usize {
    if (output.len < input.len * 2) return error.OutputTooSmall;
    // escreve no buffer do chamador
    return input.len * 2;
}
```

No lado Node, você calcula tamanho máximo, cria um `Buffer`, passa para o addon e depois corta para o tamanho real. Isso é menos elegante que retornar uma string mágica, mas é previsível.

## Erros: converta para JavaScript cedo

Zig incentiva erros explícitos; JavaScript espera exceções ou objetos `Result`. Escolha uma convenção e mantenha.

Para bibliotecas internas, eu prefiro exceções para erro de uso e retorno estruturado para erro esperado de domínio:

```ts
// Erro de uso: tipo errado, argumento ausente
native.parse("texto"); // lança TypeError, esperava Buffer

// Erro esperado: arquivo inválido, payload recusado
const result = native.parsePayload(buffer);
if (!result.ok) console.error(result.reason);
```

No Zig, mapeie erros conhecidos para mensagens estáveis. Não vaze `error.InvalidState42` como contrato público se você pretende mudar a implementação.

## Build com Zig

O addon precisa ser uma biblioteca dinâmica com extensão `.node`. O `build.zig` varia conforme plataforma, mas o conceito é:

```zig
const std = @import("std");

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

    const lib = b.addSharedLibrary(.{
        .name = "meu_addon",
        .root_source_file = b.path("src/addon.zig"),
        .target = target,
        .optimize = optimize,
    });

    lib.linkLibC();
    lib.addIncludePath(b.path("../node-headers/include/node"));
    b.installArtifact(lib);
}
```

No empacotamento final, renomeie a saída para `meu_addon.node` e coloque em `prebuilds/<platform>-<arch>/`. Combine isso com uma matriz de CI como a do guia [GitHub Actions para release multiplataforma](/artigos/zig-github-actions-release-multiplataforma/).

## Empacotamento npm

Para pacote privado ou público, evite compilar no `postinstall` por padrão. Compilação no computador do usuário quebra quando falta Python, compilador C, header de Node, permissões ou rede. Prefira binários pré-compilados.

Um fluxo prático:

1. CI roda `zig build test`.
2. CI compila `.node` para os targets suportados.
3. CI empacota `prebuilds/linux-x64`, `prebuilds/linux-arm64`, `prebuilds/darwin-arm64`, `prebuilds/darwin-x64` e `prebuilds/win32-x64`.
4. `loader.ts` escolhe o binário em runtime.
5. O pacote documenta fallback manual para quem quer compilar localmente.

Se o addon é usado apenas dentro da sua empresa, o mesmo padrão funciona em registry privado. O importante é não transformar instalação em build imprevisível.

## Performance: meça a fronteira

Nem todo trecho nativo melhora performance. A chamada JS -> N-API tem custo. Copiar buffers tem custo. Serializar JSON para atravessar a fronteira pode custar mais que executar tudo em JavaScript.

Boas medições:

- compare função JS pura, addon Zig e versão com cópia mínima;
- rode payloads pequenos, médios e grandes;
- separe tempo de conversão de tempo do algoritmo;
- meça p95/p99, não só média;
- rode em `ReleaseFast` e `ReleaseSafe` quando segurança em runtime importa;
- inclua teste de memória e handles abertos.

Para metodologia, use o guia [benchmarking em Zig](/artigos/zig-benchmarking-medir-performance/) e, se o serviço Node expõe API, compare também com [observabilidade em Zig](/artigos/zig-observabilidade/) para definir sinais de produção.

## Checklist antes de colocar em produção

Antes de depender de um addon Zig em Node.js, confira:

- [ ] wrapper TypeScript valida argumentos antes de chamar nativo;
- [ ] núcleo Zig tem testes sem Node;
- [ ] ponte N-API trata todos os status relevantes;
- [ ] nenhum ponteiro para memória temporária escapa;
- [ ] buffers grandes evitam cópias desnecessárias;
- [ ] erros têm mensagens estáveis;
- [ ] CI compila os targets suportados;
- [ ] pacote não depende de build no `postinstall` sem fallback claro;
- [ ] benchmark prova ganho suficiente para justificar complexidade;
- [ ] logs/telemetria mostram falhas de carregamento do binário.

## Exemplo de bons casos de uso

Alguns módulos onde Zig pode pagar a complexidade:

### Parser de log ou protocolo

Node recebe upload, fila ou stream. Zig parseia o formato binário, valida checksums e retorna offsets/erros. O JavaScript decide o fluxo de produto.

### Validador de payload em lote

Um SaaS recebe arquivos CSV/JSONL grandes. Zig valida tipos, datas e limites em buffer, devolvendo lista compacta de erros. TypeScript transforma isso em relatório humano.

### Codec ou compressão específica

Quando a operação aparece em todo request ou job, remover overhead de JavaScript e controlar alocação pode reduzir latência de cauda.

### Wrapper de biblioteca C

Se a empresa já depende de uma biblioteca C estável, Zig pode oferecer uma camada mais segura e um pacote npm com binários pré-compilados, em vez de espalhar `node-gyp` pelo time.

## Quando WebAssembly é melhor

Se você precisa rodar o mesmo núcleo no navegador e no servidor, ou quer sandbox forte por padrão, WebAssembly pode ser melhor que N-API. O custo é outro: ABI mais limitada, integração com arquivos/rede menos direta e tooling diferente.

Uma regra prática:

- **N-API** para integração profunda com Node, buffers, bibliotecas C e máxima performance no servidor.
- **Wasm** para sandbox, portabilidade entre browser/edge/server e fronteira de capacidades mais rígida.
- **Processo separado** para isolamento operacional, quando crash nativo não pode derrubar o processo Node.

O melhor projeto às vezes usa os três: TypeScript para produto, Zig como CLI ou addon em pontos quentes e Wasm para plugins de terceiros.

## Erros comuns

1. **Mover código cedo demais.** Primeiro prove o gargalo com profiler.
2. **Passar JSON enorme pela fronteira.** Prefira Buffer e formatos compactos.
3. **Compilar no postinstall sem plano B.** Usuário não quer depurar toolchain.
4. **Misturar regra de negócio no addon.** A ponte deve ser pequena e técnica.
5. **Ignorar ABI por plataforma.** Linux glibc, musl, macOS e Windows têm diferenças reais.
6. **Esquecer finalizers.** Estado nativo sem dono vira vazamento silencioso.
7. **Não testar falha de carregamento.** O loader deve explicar qual binário faltou.

## Conclusão

Zig não precisa substituir Node.js para ser útil em um produto Node. O uso mais forte é cirúrgico: manter TypeScript onde ele é produtivo e escrever em Zig o pedaço que processa bytes, conversa com C ou precisa de distribuição nativa previsível.

Comece com uma função pequena, benchmark claro e wrapper honesto. Se o ganho aparece mesmo depois do custo de fronteira, evolua para CI com binários pré-compilados. Se o ganho não aparece, ótimo: você provou que Node já estava bom o suficiente e evitou complexidade desnecessária.

Para continuar, leia [CLI profissional em Zig](/artigos/zig-cli-aplicacao-linha-comando/), [release multiplataforma com GitHub Actions](/artigos/zig-github-actions-release-multiplataforma/) e [Zig vs Go para backend e infraestrutura](/artigos/zig-vs-go/).
