Protobuf is one of the best API description formats available — strongly typed, language-agnostic, compact, and fast. But the tooling around it has always been painful. protoc requires manually managing a binary, wiring together plugins with obscure flags, installing language runtimes for each plugin, and running fragile shell scripts to generate code. There’s no linting, no breaking change detection, no registry for sharing schemas, and no way to know if the change you just made silently broke a consumer.
Buf fixes all of this. It’s a CLI tool and schema registry that brings the same quality of tooling to Protobuf that Cargo, npm, and Go modules brought to their respective ecosystems. This guide covers the full workflow: linting, breaking change detection, code generation with remote plugins, the Buf Schema Registry, connect-go for modern gRPC servers, and CI/CD integration.
What’s Wrong with protoc
A typical protoc invocation looks like this:
1
2
3
4
5
6
7
8
9
|
protoc \
--proto_path=. \
--proto_path=$GOPATH/src \
--proto_path=$GOPATH/src/github.com/grpc-ecosystem/grpc-gateway/third_party/googleapis \
--go_out=paths=source_relative:./gen \
--go-grpc_out=paths=source_relative:./gen \
--grpc-gateway_out=paths=source_relative:./gen \
--openapiv2_out=./gen \
$(find . -name '*.proto' -not -path './vendor/*')
|
Problems with this approach:
- Plugin management is manual — you need
protoc-gen-go, protoc-gen-go-grpc, protoc-gen-grpc-gateway, and more installed at the right versions, in $PATH
- No linting — nothing stops you from naming a field
MyField instead of my_field, or using int32 for a user ID that should be a string
- No breaking change detection — renaming a field or changing its type silently breaks wire compatibility for existing clients
- No registry — sharing schemas across teams means copying
.proto files or vendoring entire repos
- Slow — protoc invocations are not cached; every build re-compiles everything
Buf addresses each of these.
Installation
1
2
3
4
5
6
7
8
9
10
11
12
|
# macOS
brew install bufbuild/buf/buf
# Linux (direct binary)
curl -sSL https://github.com/bufbuild/buf/releases/latest/download/buf-Linux-x86_64 \
-o /usr/local/bin/buf && chmod +x /usr/local/bin/buf
# Verify
buf --version
# As a Go tool (pinned version in go.mod)
go install github.com/bufbuild/buf/cmd/buf@latest
|
Project Structure
A typical Buf workspace:
my-api/
├── buf.yaml # Module configuration
├── buf.gen.yaml # Code generation configuration
├── buf.lock # Dependency lockfile (auto-generated)
└── proto/
└── acme/
└── petstore/
└── v1/
├── petstore.proto
└── petstore_service.proto
buf.yaml
buf.yaml defines the module — the root of a set of Protobuf files that are versioned and published together.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
|
version: v2
# Module name (required for publishing to BSR)
name: buf.build/acme/petstore
# External dependencies from the Buf Schema Registry
deps:
- buf.build/googleapis/googleapis # google/api annotations
- buf.build/grpc-ecosystem/grpc-gateway # grpc-gateway annotations
# Linting configuration
lint:
use:
- DEFAULT # All DEFAULT category rules
except:
- FIELD_NOT_REQUIRED # Disable specific rules
ignore:
- proto/acme/petstore/v1/legacy.proto # Skip specific files
# Breaking change detection configuration
breaking:
use:
- FILE # FILE category (most common)
ignore_unstable_packages: true # Ignore packages with alpha/beta/v0 suffixes
|
buf.gen.yaml
buf.gen.yaml configures code generation — which plugins to use, where to write output.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
|
version: v2
# Input (what to generate from)
inputs:
- directory: proto
# Plugins to run
plugins:
# Remote plugin from BSR (no local install needed)
- remote: buf.build/protocolbuffers/go
out: gen/go
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: gen/go
opt:
- paths=source_relative
- require_unimplemented_servers=false
# connect-go: modern gRPC/Connect server and client
- remote: buf.build/connectrpc/go
out: gen/go
opt:
- paths=source_relative
# TypeScript with connect-es
- remote: buf.build/bufbuild/es
out: gen/ts
opt:
- target=ts
- remote: buf.build/connectrpc/es
out: gen/ts
opt:
- target=ts
|
Linting
Run the linter:
1
2
3
4
5
6
7
|
buf lint
# Lint a specific directory
buf lint proto/
# Output as JSON (useful in CI)
buf lint --error-format=json
|
Example output:
proto/acme/petstore/v1/petstore.proto:15:3:Field name "petID" should be lower_snake_case, such as "pet_id".
proto/acme/petstore/v1/petstore.proto:22:1:Message name "getPetRequest" should be PascalCase, such as "GetPetRequest".
Rule categories
Buf organizes lint rules into categories. Each higher category is a superset of the lower:
| Category |
Rules |
Use case |
MINIMAL |
~10 rules |
Absolute minimum; prevents wire incompatibility |
BASIC |
~30 rules |
Good defaults for new projects |
DEFAULT |
~50 rules |
Recommended; enforces Buf style guide |
Key DEFAULT rules
Naming conventions:
FIELD_LOWER_SNAKE_CASE — pet_id, not petID or PetId
MESSAGE_PASCAL_CASE — GetPetRequest, not get_pet_request
ENUM_VALUE_PREFIX — enum values must be prefixed: PET_STATUS_AVAILABLE, not AVAILABLE
ENUM_VALUE_UPPER_SNAKE_CASE — PET_STATUS_AVAILABLE, not Pet_Status_Available
SERVICE_SUFFIX — services must end in Service: PetStoreService, not PetStore
RPC_REQUEST_RESPONSE_UNIQUE — each RPC has its own request/response message type
RPC_REQUEST_STANDARD_NAME — request type is {RpcName}Request
RPC_RESPONSE_STANDARD_NAME — response type is {RpcName}Response
Design rules:
PACKAGE_VERSION_SUFFIX — stable packages must have a version suffix: acme.petstore.v1
PACKAGE_SAME_DIRECTORY — all files in a package live in the same directory
IMPORT_NO_WEAK — no weak imports
ENUM_NO_ALLOW_ALIAS — no allow_alias = true on enums
Why these matter: The ENUM_VALUE_PREFIX rule prevents the classic Protobuf footgun where two enums in the same package both have a value named UNKNOWN. Protobuf enums share a namespace within a package, so UNKNOWN = 0 in two different enums in the same package is a compilation error. Prefixing avoids this entirely.
Configuring rules
1
2
3
4
5
6
7
8
9
10
11
|
# buf.yaml
lint:
use:
- DEFAULT
except:
# Allow repeated request/response types across RPCs (legacy APIs)
- RPC_REQUEST_RESPONSE_UNIQUE
ignore_only:
# Allow non-standard naming in legacy files only
FIELD_LOWER_SNAKE_CASE:
- proto/acme/petstore/v1/legacy.proto
|
Breaking Change Detection
This is Buf’s most operationally valuable feature. Run it against the previous version of your API to find incompatibilities before they reach production.
1
2
3
4
5
6
7
8
9
10
11
|
# Compare against a git tag
buf breaking --against ".git#tag=v1.2.0"
# Compare against the main branch
buf breaking --against ".git#branch=main"
# Compare against the BSR (if you've pushed your module)
buf breaking --against "buf.build/acme/petstore:main"
# Compare against a specific commit
buf breaking --against ".git#ref=abc1234"
|
What counts as breaking
The FILE breaking category (most commonly used) flags changes that break binary wire compatibility or generated code compatibility:
Always breaking:
- Deleting a field
- Changing a field’s number
- Changing a field’s type (e.g.,
int32 → int64, string → bytes)
- Changing a field from singular to repeated (or vice versa)
- Deleting an enum value
- Changing an enum value’s number
- Deleting an RPC method
- Changing an RPC’s request or response type
- Changing an RPC from unary to streaming (or vice versa)
Not breaking (safe to do):
- Adding a new field (with a new field number)
- Adding a new enum value
- Adding a new RPC method
- Adding a new message type
- Adding a new service
- Changing a field’s
json_name option (wire format is unchanged)
- Adding or changing comments
Example output:
proto/acme/petstore/v1/petstore.proto:18:3:Field "1" with name "id" on message "Pet" changed type from "int64" to "string".
proto/acme/petstore/v1/petstore.proto:24:3:Field "3" with name "name" on message "Pet" was deleted.
The WIRE_JSON category
If your consumers use JSON serialization (grpc-gateway, Connect’s JSON mode), use WIRE_JSON instead of FILE — it additionally checks json_name option changes and other JSON-specific incompatibilities.
1
2
3
4
|
# buf.yaml
breaking:
use:
- WIRE_JSON
|
Code Generation
Remote plugins (no local installs needed)
The killer feature of buf generate with remote plugins: you don’t need protoc-gen-go, protoc-gen-go-grpc, or any other plugin binary installed locally. Buf runs them remotely on its servers and caches the results.
1
2
3
4
5
6
7
8
|
# Generate all code defined in buf.gen.yaml
buf generate
# Generate from a specific proto directory
buf generate proto/
# Generate from the BSR (someone else's schema)
buf generate buf.build/googleapis/googleapis --template buf.gen.yaml
|
Local plugins (for private or custom plugins)
1
2
3
4
5
6
7
8
9
10
11
|
# buf.gen.yaml
plugins:
# Local binary (must be in PATH or specify full path)
- local: protoc-gen-go
out: gen/go
opt:
- paths=source_relative
# Local binary with full path
- local: /usr/local/bin/protoc-gen-mycompany
out: gen/mycompany
|
Full Go example
proto/acme/petstore/v1/petstore.proto:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
syntax = "proto3";
package acme.petstore.v1;
option go_package = "github.com/acme/petstore/gen/go/acme/petstore/v1;petstoreV1";
message Pet {
string id = 1;
string name = 2;
string status = 3;
repeated string photo_urls = 4;
}
message GetPetRequest {
string pet_id = 1;
}
message GetPetResponse {
Pet pet = 1;
}
message ListPetsRequest {
int32 page_size = 1;
string page_token = 2;
}
message ListPetsResponse {
repeated Pet pets = 1;
string next_page_token = 2;
}
service PetStoreService {
rpc GetPet(GetPetRequest) returns (GetPetResponse);
rpc ListPets(ListPetsRequest) returns (ListPetsResponse);
}
|
Run buf generate to produce Go code. With connect-go in buf.gen.yaml, you get three output files per service: the message types (petstore.pb.go), the gRPC stubs (petstore_grpc.pb.go), and the Connect handlers (petstore_connect.pb.go).
connect-go: Modern gRPC for Go
connect-go is Buf’s replacement for grpc-go. It supports three protocols over the same handler:
| Protocol |
Transport |
Clients |
| Connect |
HTTP/1.1 + HTTP/2 |
curl, browsers, any HTTP client |
| gRPC |
HTTP/2 |
All existing gRPC clients |
| gRPC-Web |
HTTP/1.1 |
Browsers via gRPC-Web proxy |
The key advantage: Connect servers work behind any standard HTTP proxy (Nginx, Traefik, AWS ALB) without special configuration, because they use standard HTTP/1.1 alongside HTTP/2. You don’t need an HTTP/2 load balancer for your API.
Server implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
|
package main
import (
"context"
"fmt"
"net/http"
"connectrpc.com/connect"
petstoreV1 "github.com/acme/petstore/gen/go/acme/petstore/v1"
"github.com/acme/petstore/gen/go/acme/petstore/v1/petstoreV1connect"
)
// PetStoreServer implements the generated interface.
type PetStoreServer struct{}
func (s *PetStoreServer) GetPet(
ctx context.Context,
req *connect.Request[petstoreV1.GetPetRequest],
) (*connect.Response[petstoreV1.GetPetResponse], error) {
// Access request headers
fmt.Println("Authorization:", req.Header().Get("Authorization"))
pet := &petstoreV1.Pet{
Id: req.Msg.PetId,
Name: "Fido",
Status: "available",
}
res := connect.NewResponse(&petstoreV1.GetPetResponse{Pet: pet})
// Set response headers
res.Header().Set("Grpc-Status-Details-Bin", "")
return res, nil
}
func (s *PetStoreServer) ListPets(
ctx context.Context,
req *connect.Request[petstoreV1.ListPetsRequest],
) (*connect.Response[petstoreV1.ListPetsResponse], error) {
return connect.NewResponse(&petstoreV1.ListPetsResponse{
Pets: []*petstoreV1.Pet{
{Id: "1", Name: "Fido", Status: "available"},
{Id: "2", Name: "Whiskers", Status: "pending"},
},
}), nil
}
func main() {
mux := http.NewServeMux()
// Register the handler — returns path and handler
path, handler := petstoreV1connect.NewPetStoreServiceHandler(&PetStoreServer{})
mux.Handle(path, handler)
// Standard net/http server — works with any middleware
if err := http.ListenAndServe(":8080", mux); err != nil {
panic(err)
}
}
|
Client implementation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
package main
import (
"context"
"fmt"
"net/http"
"connectrpc.com/connect"
petstoreV1 "github.com/acme/petstore/gen/go/acme/petstore/v1"
"github.com/acme/petstore/gen/go/acme/petstore/v1/petstoreV1connect"
)
func main() {
client := petstoreV1connect.NewPetStoreServiceClient(
http.DefaultClient,
"http://localhost:8080",
// Use gRPC protocol instead of Connect:
// connect.WithGRPC(),
)
res, err := client.GetPet(context.Background(),
connect.NewRequest(&petstoreV1.GetPetRequest{PetId: "1"}),
)
if err != nil {
panic(err)
}
fmt.Printf("Pet: %s (%s)\n", res.Msg.Pet.Name, res.Msg.Pet.Status)
}
|
Middleware and interceptors
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
|
// Add interceptors for logging, auth, metrics
path, handler := petstoreV1connect.NewPetStoreServiceHandler(
&PetStoreServer{},
connect.WithInterceptors(
authInterceptor(),
loggingInterceptor(),
metricsInterceptor(),
),
)
func authInterceptor() connect.Interceptor {
return connect.UnaryInterceptorFunc(func(next connect.UnaryFunc) connect.UnaryFunc {
return func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {
token := req.Header().Get("Authorization")
if token == "" {
return nil, connect.NewError(connect.CodeUnauthenticated, fmt.Errorf("missing token"))
}
return next(ctx, req)
}
})
}
|
connect-es: TypeScript Clients
With buf.gen.yaml configured for connect-es, buf generate produces TypeScript clients that work in browsers and Node.js.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
// Generated client usage (React/browser example)
import { createClient } from "@connectrpc/connect";
import { createConnectTransport } from "@connectrpc/connect-web";
import { PetStoreService } from "./gen/ts/acme/petstore/v1/petstore_connect";
const transport = createConnectTransport({
baseUrl: "https://api.example.com",
});
const client = createClient(PetStoreService, transport);
// Unary call
const response = await client.getPet({ petId: "1" });
console.log(response.pet?.name);
// Works with both Connect and gRPC-Web transports:
// import { createGrpcWebTransport } from "@connectrpc/connect-web";
|
For Node.js servers:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
|
import { createServer } from "@connectrpc/connect-node";
import { PetStoreService } from "./gen/ts/acme/petstore/v1/petstore_connect";
const server = createServer({
routes: (router) =>
router.service(PetStoreService, {
async getPet(req) {
return { pet: { id: req.petId, name: "Fido", status: "available" } };
},
async listPets(req) {
return { pets: [], nextPageToken: "" };
},
}),
});
server.listen({ port: 8080 });
|
The Buf Schema Registry (BSR)
The BSR is a hosted registry for Protobuf modules — think npm or Docker Hub, but for schemas. Free for public modules.
Pushing a module
1
2
3
4
5
6
7
8
|
# Login (creates ~/.config/buf/credentials.json)
buf registry login
# Push current module to BSR
buf push
# Push with a label (like a git tag)
buf push --label v1.2.0
|
Your module is now at buf.build/acme/petstore and anyone can depend on it.
Depending on BSR modules
Add dependencies to buf.yaml and run buf dep update to generate the lockfile:
1
2
3
4
5
|
# buf.yaml
deps:
- buf.build/googleapis/googleapis
- buf.build/grpc-ecosystem/grpc-gateway
- buf.build/acme/shared-types # Internal shared schemas
|
1
2
|
buf dep update # Resolves and locks versions → buf.lock
buf dep prune # Remove unused dependencies
|
Remote plugins on the BSR
Instead of installing protoc plugins locally, reference them by name in buf.gen.yaml:
1
2
3
4
5
6
7
8
|
plugins:
# Latest stable version
- remote: buf.build/protocolbuffers/go
out: gen/go
# Pin to a specific version
- remote: buf.build/protocolbuffers/go:v1.34.0
out: gen/go
|
The BSR maintains plugins for all major languages. Your team members get identical output without installing anything — Buf handles plugin versioning and execution remotely.
Generated SDKs
The BSR can generate and host language-specific SDKs directly. Consumers can install your Go or TypeScript SDK as a regular package dependency — without running buf generate at all.
For Go:
1
2
3
|
# BSR-generated Go SDK — install directly with go get
go get buf.build/gen/go/acme/petstore/protocolbuffers/go
go get buf.build/gen/go/acme/petstore/connectrpc/go
|
For npm:
1
2
|
npm install @buf/acme_petstore.bufbuild_es
npm install @buf/acme_petstore.connectrpc_es
|
This eliminates the “check in generated code or generate on the fly?” debate entirely — the registry serves pre-generated, versioned SDKs.
buf curl: Command-Line RPC Calls
buf curl makes gRPC and Connect calls from the terminal, similar to curl for REST:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
# Unary call to a Connect server
buf curl \
--data '{"pet_id": "1"}' \
https://api.example.com/acme.petstore.v1.PetStoreService/GetPet
# Use gRPC protocol instead
buf curl \
--protocol grpc \
--data '{"pet_id": "1"}' \
https://api.example.com/acme.petstore.v1.PetStoreService/GetPet
# With custom headers
buf curl \
--header "Authorization: Bearer $TOKEN" \
--data '{"pet_id": "1"}' \
https://api.example.com/acme.petstore.v1.PetStoreService/GetPet
# Against a local server (no TLS)
buf curl \
--http2-prior-knowledge \
--data '{"page_size": 10}' \
http://localhost:8080/acme.petstore.v1.PetStoreService/ListPets
# List all available methods (uses server reflection)
buf curl \
--list-methods \
https://api.example.com
# Use a local proto file instead of reflection
buf curl \
--schema proto/ \
--data '{"pet_id": "1"}' \
http://localhost:8080/acme.petstore.v1.PetStoreService/GetPet
|
buf curl automatically uses server reflection if available, so you don’t need to specify the schema for servers that expose it.
CI/CD Integration
GitHub Actions
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
|
# .github/workflows/buf.yaml
name: Buf
on:
push:
branches: [main]
pull_request:
branches: [main]
jobs:
lint:
name: Lint
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
lint: true
breaking:
name: Breaking Change Detection
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
# Compare against the target branch of the PR
breaking: true
breaking_against: "https://github.com/${{ github.repository }}.git#branch=${{ github.base_ref }}"
push:
name: Push to BSR
runs-on: ubuntu-latest
# Only push on merges to main
if: github.ref == 'refs/heads/main' && github.event_name == 'push'
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
push: true
env:
BUF_TOKEN: ${{ secrets.BUF_TOKEN }}
|
The bufbuild/buf-action action handles installation automatically. Set BUF_TOKEN in your repository secrets (generate at buf.build → Settings → Tokens).
Complete workflow with generate check
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
|
name: Buf
on: [pull_request, push]
jobs:
buf:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: bufbuild/buf-action@v1
with:
lint: true
format: true
breaking: true
breaking_against: ".git#branch=main,subdir=proto"
push: ${{ github.event_name == 'push' && github.ref == 'refs/heads/main' }}
env:
BUF_TOKEN: ${{ secrets.BUF_TOKEN }}
# Verify generated code is up to date
- name: Verify generated code
run: |
buf generate
git diff --exit-code gen/
|
The last step ensures developers have committed up-to-date generated code — if they forgot to run buf generate after editing a .proto file, CI fails.
Migrating from protoc
Replace this:
1
2
3
4
5
6
7
|
# Old: protoc invocation
protoc \
--proto_path=. \
--proto_path=$GOPATH/src \
--go_out=paths=source_relative:./gen/go \
--go-grpc_out=paths=source_relative:./gen/go \
$(find proto/ -name '*.proto')
|
With this:
buf.yaml:
buf.gen.yaml:
1
2
3
4
5
6
7
8
9
10
11
12
|
version: v2
inputs:
- directory: proto
plugins:
- remote: buf.build/protocolbuffers/go
out: gen/go
opt:
- paths=source_relative
- remote: buf.build/grpc/go
out: gen/go
opt:
- paths=source_relative
|
Then:
That’s it. No protoc binary, no plugin binaries, no $PROTO_PATH juggling.
Migrating existing imports
If your .proto files use import "google/api/annotations.proto", you need to tell Buf where to find it. Replace the local copy with a BSR dependency:
1
2
3
|
# buf.yaml
deps:
- buf.build/googleapis/googleapis
|
Now remove your vendored google/ proto directory. Buf resolves the dependency from the BSR.
Workspaces (Monorepos)
For monorepos with multiple Protobuf modules:
monorepo/
├── buf.yaml # Workspace root
└── proto/
├── users/
│ ├── buf.yaml # Module: buf.build/acme/users
│ └── acme/users/v1/
└── orders/
├── buf.yaml # Module: buf.build/acme/orders (depends on users)
└── acme/orders/v1/
Root buf.yaml:
1
2
3
4
5
|
version: v2
# Workspace: references all local modules
modules:
- path: proto/users
- path: proto/orders
|
Each sub-module has its own buf.yaml with its name and deps. When the orders module depends on the users module, during local development Buf resolves it from the local workspace rather than the BSR — no need to push intermediate versions just to test locally.
Buf includes an opinionated formatter:
1
2
3
4
5
6
7
8
|
# Check formatting (exit code 1 if any file is unformatted)
buf format --diff
# Write formatted output in place
buf format --write
# Format a specific file
buf format proto/acme/petstore/v1/petstore.proto --write
|
Add to CI alongside lint:
Putting It All Together
A complete Makefile (or Taskfile) for a Buf-managed project:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
|
.PHONY: proto lint breaking generate
proto: lint generate
lint:
buf lint
buf format --exit-code
breaking:
buf breaking --against ".git#branch=main"
generate:
buf generate
push:
buf push --label $(shell git describe --tags --always)
deps:
buf dep update
|
The developer workflow becomes:
- Edit
.proto files
buf lint — fix any style violations
buf breaking --against ".git#branch=main" — verify no compatibility breaks
buf generate — regenerate client/server code
- Implement changes in Go/TypeScript against the new generated code
- Open PR → CI runs lint, breaking detection, and generate check automatically
- Merge → CI pushes new module version to BSR
Summary
Buf replaces a fragile ecosystem of manually-managed binaries with a single tool that handles the entire Protobuf workflow:
| Problem |
Buf solution |
| Managing protoc and plugin binaries |
Remote plugins via BSR |
No linting for .proto files |
buf lint with configurable rule categories |
| No breaking change detection |
buf breaking with FILE/WIRE_JSON categories |
| Sharing schemas across teams |
Buf Schema Registry with versioned modules |
| Generated SDKs for consumers |
BSR-hosted generated SDKs (go get / npm install) |
| Complex protoc invocations |
Simple buf generate with buf.gen.yaml |
| HTTP/2-only gRPC servers |
connect-go: HTTP/1.1 + HTTP/2 + gRPC from one handler |
| Testing gRPC APIs from the CLI |
buf curl |
For new projects, start with buf lint, buf generate, and the BSR from day one. For existing projects, migration is straightforward — add buf.yaml, replace your protoc invocations with buf generate, and wire up CI. The breaking change detector alone pays for the migration cost the first time it catches a field rename before it ships.
Comments