Protocol Buffers & Buf CLI Reference: Messages, Services, Code Generation & Connect
Protocol Buffers (protobuf) is Google’s binary serialisation format — 3-10x smaller than JSON, 5-10x faster to parse. You define a .proto schema and generate typed code for any language. Buf CLI is the modern toolchain: it replaces raw protoc invocations, enforces API compatibility rules, handles remote module dependencies, and runs a schema registry. The workflow: write .proto files → buf generate → use generated stubs in your gRPC server and clients.
1. .proto Syntax — Messages, Fields & Scalar Types
Message definitions, field numbers, optional/repeated/map, scalar types, and imports
// proto/user/v1/user.proto
syntax = "proto3";
package user.v1;
option go_package = "github.com/example/api/gen/go/user/v1;userv1";
option java_package = "com.example.user.v1";
import "google/protobuf/timestamp.proto";
import "google/protobuf/field_mask.proto";
// Field numbers are permanent — never reuse deleted numbers!
message User {
string id = 1; // field number — used in wire format
string name = 2;
string email = 3;
// optional: explicit presence tracking (proto3 — tells if field was set or is default)
optional int32 age = 4;
// repeated: ordered list (like an array)
repeated string tags = 5;
// map: key/value pairs (keys: integral or string types only)
map<string, string> metadata = 6;
// Nested message:
Address address = 7;
// Well-known type from google.protobuf:
google.protobuf.Timestamp created_at = 8;
// Enum:
UserRole role = 9;
}
enum UserRole {
USER_ROLE_UNSPECIFIED = 0; // always define a zero value
USER_ROLE_VIEWER = 1;
USER_ROLE_EDITOR = 2;
USER_ROLE_ADMIN = 3;
}
message Address {
string street = 1;
string city = 2;
string country_code = 3; // snake_case in proto, camelCase in JSON
}
// Scalar types:
// string, bytes — UTF-8 / raw bytes
// bool — true/false
// int32, int64 — signed (inefficient for negative numbers)
// uint32, uint64 — unsigned
// sint32, sint64 — signed, ZigZag encoded (efficient for negative)
// fixed32, fixed64 — always 4/8 bytes (for values >2^28)
// float, double — IEEE 754
2. Services & RPCs
Define gRPC services, unary and streaming RPCs, and well-known types for CRUD
// proto/user/v1/user_service.proto
syntax = "proto3";
package user.v1;
import "user/v1/user.proto";
import "google/protobuf/empty.proto";
import "google/protobuf/field_mask.proto";
service UserService {
// Unary RPC — one request, one response:
rpc GetUser(GetUserRequest) returns (GetUserResponse);
rpc CreateUser(CreateUserRequest) returns (CreateUserResponse);
rpc UpdateUser(UpdateUserRequest) returns (UpdateUserResponse);
rpc DeleteUser(DeleteUserRequest) returns (google.protobuf.Empty);
// Server streaming — one request, stream of responses:
rpc ListUsers(ListUsersRequest) returns (stream ListUsersResponse);
// Client streaming — stream of requests, one response:
rpc BatchCreateUsers(stream CreateUserRequest) returns (BatchCreateUsersResponse);
// Bidirectional streaming:
rpc Chat(stream ChatMessage) returns (stream ChatMessage);
}
message GetUserRequest { string id = 1; }
message GetUserResponse { User user = 1; }
message CreateUserRequest { string name = 1; string email = 2; }
message CreateUserResponse{ User user = 1; }
message DeleteUserRequest { string id = 1; }
// FieldMask for partial updates (best practice for UpdateUser):
message UpdateUserRequest {
User user = 1;
google.protobuf.FieldMask update_mask = 2; // which fields to update
}
// Pagination pattern:
message ListUsersRequest {
int32 page_size = 1;
string page_token = 2; // cursor-based, not offset
string filter = 3;
string order_by = 4;
}
message ListUsersResponse {
repeated User users = 1;
string next_page_token = 2; // empty = last page
int32 total_size = 3;
}
3. Buf CLI — Modern Protobuf Toolchain
buf.yaml, buf.gen.yaml, buf generate, buf lint, buf breaking, and BSR registry
# Install:
# brew install bufbuild/buf/buf # macOS
# npm install -g @bufbuild/buf # Node.js
# buf.yaml — workspace config (project root):
version: v2
modules:
- path: proto # where your .proto files live
deps:
- buf.build/googleapis/googleapis # google.protobuf.Timestamp etc.
- buf.build/grpc-ecosystem/grpc-gateway # HTTP/JSON transcoding
lint:
use:
- DEFAULT
except:
- PACKAGE_VERSION_SUFFIX # if you're not using v1/v2 versioning
breaking:
use:
- FILE # check for breaking changes at file level
# buf.gen.yaml — code generation config:
version: v2
plugins:
- remote: buf.build/protocolbuffers/go # Go messages
out: gen/go
opt: paths=source_relative
- remote: buf.build/grpc/go # Go gRPC server/client
out: gen/go
opt: paths=source_relative,require_unimplemented_servers=false
- remote: buf.build/connectrpc/go # Connect protocol (HTTP/1.1 + HTTP/2)
out: gen/go
opt: paths=source_relative
- remote: buf.build/bufbuild/es # TypeScript (Connect/Fetch)
out: gen/ts
# Generate code:
buf dep update # download dependencies to buf.lock
buf generate # generate code from all .proto files
# Lint .proto files:
buf lint # checks naming, field presence, import style
buf lint proto/ # lint a specific directory
# Check for breaking changes (against a git tag or BSR module):
buf breaking --against ".git#tag=v1.0.0"
buf breaking --against "buf.build/myorg/myapi:v1.0.0"
# Push to Buf Schema Registry:
buf push --tag v1.0.1 # versioned module on buf.build
4. Encoding, JSON Mapping & Field Masks
Binary encoding gotchas, JSON transcoding, field mask partial updates, and oneof
// Wire encoding key rules:
// - Default values are NOT transmitted (int32=0, string="", bool=false are omitted)
// Use optional to distinguish "not set" from "set to default"
// - Field numbers never change — adding new fields is safe, removing is not
// - Renaming fields is safe (only field numbers matter in binary)
// - NEVER reuse field numbers of deleted fields (store in reserved)
message UserV2 {
reserved 10, 11; // old field numbers — never reuse
reserved "legacy_name"; // old field names
string id = 1;
string name = 2;
}
// JSON mapping (for REST/HTTP APIs via grpc-gateway or Connect):
// snake_case → camelCase: user_id → "userId"
// google.protobuf.Timestamp → ISO 8601 string: "2026-03-14T12:00:00Z"
// bytes → base64 string
// enum → string name: USER_ROLE_ADMIN → "USER_ROLE_ADMIN"
// map<string, string> → JSON object
// oneof — exactly one of the listed fields is set:
message ApiResponse {
oneof result {
User user = 1;
Error error = 2;
}
}
// In Go: switch resp.Result.(type) { case *ApiResponse_User: case *ApiResponse_Error: }
// In TypeScript: if (resp.result.case === "user") { resp.result.value.id }
// FieldMask partial update (UpdateUserRequest):
// Client sends: { user: { name: "Alice Smith" }, update_mask: { paths: ["name"] } }
// Server applies only the masked fields — ignores unset fields
// In Go: proto.Merge handles FieldMask with fieldmaskpb.Intersect
5. Connect Protocol & TypeScript Client
Connect-RPC (HTTP/1.1 compatible), TypeScript client with @bufbuild/connect, and interceptors
// Connect protocol: works over HTTP/1.1 (unlike gRPC which needs HTTP/2)
// Supports gRPC, gRPC-Web, and Connect JSON protocols — ideal for browser clients
// Generated TypeScript client (buf generate with @bufbuild/es + @connectrpc/connect):
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { UserService } from "./gen/ts/user/v1/user_service_connect";
const transport = createConnectTransport({
baseUrl: "https://api.example.com",
// Interceptors — logging, auth, retry:
interceptors: [
(next) => async (req) => {
req.header.set("Authorization", `Bearer ${await getToken()}`);
console.log(`[${req.method.name}]`);
const res = await next(req);
return res;
},
],
});
const client = createClient(UserService, transport);
// Unary call:
const { user } = await client.getUser({ id: "user-123" });
console.log(user.name); // typed as string
// Server streaming:
for await (const { users } of client.listUsers({ pageSize: 20 })) {
console.log(users); // batch of users per stream message
}
// React hook with @connectrpc/connect-query:
import { useQuery, useMutation } from "@connectrpc/connect-query";
function UserProfile({ id }: { id: string }) {
const { data, isLoading } = useQuery(UserService.method.getUser, { id });
return isLoading ? <Spinner /> : <div>{data?.user?.name}</div>;
}
Track Go and API tooling releases at ReleaseRun. Related: gRPC Reference | OpenAPI 3.1 Reference | TypeScript Reference
🔍 Free tool: npm Package Health Checker — check protobufjs, @bufbuild/protobuf, and related packages for known CVEs and active maintenance.
Founded
2023 in London, UK
Contact
hello@releaserun.com