Skip to content

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