ZAP Protocol

Quick Start

Get started with ZAP Protocol in 5 minutes

Quick Start

This guide builds your first ZAP service end-to-end: write a schema, generate typed code, implement a server, and call it from a client.

Prerequisites

  • Go 1.21+ or Rust 1.75+ or Node.js 20+
  • The ZAP schema compiler (installed below)

Installation

# Add the runtime
go get github.com/zap-proto/[email protected]

# Install the schema compiler (the zapgen binary)
go install github.com/zap-proto/go/cmd/[email protected]
# Install the schema compiler (provides the zapc binary)
cargo install zap-schema

# Add the runtime crates to Cargo.toml
#   zap     = { git = "https://github.com/zap-proto/rust" }
#   zapc    = { git = "https://github.com/zap-proto/rust" }
#   zap-rpc = { git = "https://github.com/zap-proto/rust" }
# The runtime ships the zapgen CLI as a bin
npm install @zap-proto/zap

# Or with pnpm
pnpm add @zap-proto/zap

Define Your Schema

Create a file named calculator.zap. ZAP is whitespace-significant — no braces, no file ID, and ordinals are assigned automatically. The legacy Cap'n Proto brace form still parses; the two produce byte-identical schemas.

calculator.zap
# ZAP schema - no file ID needed, no ordinals

struct Operands
A Float64
B Float64

struct Value
Result Float64

interface Calculator
# Methods get implicit ordinals 1, 2, 3, … in declaration order
add (req Operands) -> (resp Value)
subtract (req Operands) -> (resp Value)
multiply (req Operands) -> (resp Value)
divide (req Operands) -> (resp Value)

Generate Code

# Emit <struct>_zap.go files next to the schema
zapgen calculator.zap

# Or into a directory
zapgen -out ./gen calculator.zap
# One-shot generation with the zap-schema compiler
zapc generate calculator.zap --lang rust --out ./gen/

# Or wire it into a cargo build from build.rs:
#   zapc::CompilerCommand::new().file("calculator.zap").run().unwrap();
# zapgen ships in @zap-proto/zap; writes calculator_zap.ts
npx zapgen calculator.zap

# Or into a directory
npx zapgen -out ./src/gen calculator.zap

Generation emits a zero-copy View + Builder per struct, plus — for the interface — a typed client and an abstract server over the ZAP call envelope. See Code Generation for the full backend matrix.

Go's self-contained zapgen reads the wire-type spelling for scalars (f64, u64, f32, u32, …) and pins offsets explicitly, e.g. A f64 @0. The canonical front-end the other backends drive uses the capitalized names above (Float64, UInt64) with auto-assigned offsets; both describe the same wire layout.

Implement the Server

The generated Calculator server is an abstract handler: implement each method, and the generated dispatcher decodes the call envelope and routes by method ordinal.

package main

import (
	"errors"

	"github.com/zap-proto/go/rpc"
	"yourmodule/gen"
)

type calculator struct{}

func (calculator) Add(req []byte) ([]byte, error) {
	op, err := gen.WrapOperands(req)
	if err != nil {
		return nil, err
	}
	return gen.NewValue(gen.ValueInput{Result: op.A() + op.B()}), nil
}

func (calculator) Divide(req []byte) ([]byte, error) {
	op, err := gen.WrapOperands(req)
	if err != nil {
		return nil, err
	}
	if op.B() == 0 {
		return nil, errors.New("division by zero")
	}
	return gen.NewValue(gen.ValueInput{Result: op.A() / op.B()}), nil
}

// Subtract and Multiply follow the same shape.

// In your transport's read loop, dispatch each inbound envelope:
//   respBytes, err := gen.DispatchCalculator(calculator{}, envelope)
// then write respBytes back. The rpc package defines the envelope (the bytes);
// the transport that carries it is yours.
var _ = rpc.StatusOK
use zap_rpc::new_client;
use crate::calculator_zap::calculator;

struct CalculatorImpl;

impl calculator::Server for CalculatorImpl {
    async fn add(
        self: std::rc::Rc<Self>,
        params: calculator::AddParams,
        mut results: calculator::AddResults,
    ) -> Result<(), ::zap::Error> {
        let req = params.get()?.get_req()?;
        results.get().init_resp().set_result(req.get_a() + req.get_b());
        Ok(())
    }

    async fn divide(
        self: std::rc::Rc<Self>,
        params: calculator::DivideParams,
        mut results: calculator::DivideResults,
    ) -> Result<(), ::zap::Error> {
        let req = params.get()?.get_req()?;
        if req.get_b() == 0.0 {
            return Err(::zap::Error::failed("division by zero".into()));
        }
        results.get().init_resp().set_result(req.get_a() / req.get_b());
        Ok(())
    }

    // subtract and multiply follow the same shape.
}

// Host it as the bootstrap capability:
//   let client: calculator::Client = new_client(CalculatorImpl);
// then serve `client` over a TwoParty VatNetwork (see the Rust SDK guide).
import { CalculatorServer, Operands, newValue } from './gen/calculator_zap.js';

// Methods take and return raw ZAP bytes; wrap/build at the edges.
class Calculator extends CalculatorServer {
  async add(req: Uint8Array): Promise<Uint8Array> {
    const op = Operands.wrap(req);
    return newValue({ result: op.a + op.b }); // getters are property accessors
  }

  async divide(req: Uint8Array): Promise<Uint8Array> {
    const op = Operands.wrap(req);
    if (op.b === 0) throw new Error('division by zero');
    return newValue({ result: op.a / op.b });
  }

  // subtract and multiply follow the same shape.
}

// CalculatorServer.dispatch(envelope) decodes the ordinal and routes to the
// matching handler; drive it from your transport's read loop.

Create a Client

The generated client carries one typed method per declaration; each builds a request envelope, ships it over the channel you supply, and returns the decoded response.

package main

import (
	"fmt"

	"yourmodule/gen"
)

func main() {
	// channel is your EchoChannel/CalculatorChannel — the read/write loop that
	// ships a request envelope and awaits its correlated rpc.Response.
	calc := gen.NewCalculatorClient(channel, nil) // nil = no capability token

	// A request method returns (rpc.Promise, body, error); the promise enables
	// pipelining a dependent call before this one returns.
	_, body, err := calc.Add(gen.NewOperands(gen.OperandsInput{A: 10, B: 5}))
	if err != nil {
		panic(err)
	}
	v, _ := gen.WrapValue(body)
	fmt.Printf("10 + 5 = %.0f\n", v.Result()) // 15
}
use crate::calculator_zap::calculator;
use zap_rpc::{rpc_twoparty_zap, RpcSystem};

// `calc` is the bootstrapped remote interface (see the Rust SDK guide for
// constructing the RpcSystem over a connection).
let calc: calculator::Client = rpc_system.bootstrap(rpc_twoparty_zap::Side::Server);

let mut request = calc.add_request();
{
    let mut req = request.get().init_req();
    req.set_a(10.0);
    req.set_b(5.0);
}
let reply = request.send().promise.await?;
println!("10 + 5 = {}", reply.get()?.get_resp()?.get_result()); // 15
import { ZapClient } from '@zap-proto/zap/node';
import { CalculatorClient, newOperands, Value } from './gen/calculator_zap.js';

const conn = await ZapClient.connect('localhost:9000');
const calc = new CalculatorClient(conn);

// Methods take and return raw ZAP bytes; build the request, wrap the response.
const body = await calc.add(newOperands({ a: 10, b: 5 }));
console.log(`10 + 5 = ${Value.wrap(body).result}`); // 15

conn.close();

Next Steps

Now that you have a working ZAP service:

On this page