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/zapDefine 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.
# 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.zapGeneration 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
zapgenreads 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.StatusOKuse 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()); // 15import { 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:
- Pick a language-specific SDK guide: TypeScript, Go, Python, Rust, C++, Java, C#, Erlang, Haskell, OCaml, or C
- Learn the Schema Language and Code Generation in depth
- Understand the RPC System — the call envelope, generated stubs, and promise pipelining
- Read the Architecture and explore Transports
- Serve it with zip — the ZAP-native application server
- Carry a higher-level protocol on ZAP: HTTP, MCP, A2A, ACP, RNS, FIX, WS
- Migrating off protobuf? The tools section covers
pb2zapandzap2pb - See benchmarks — measured, reproducible numbers from the bench harness