Skip to content

Buf Schema Registry quickstart

The Buf Schema Registry (BSR) is the missing package manager for Protobuf, allowing you to manage schemas, dependencies, and governance at scale.

You’ll start with a server that has a bug, add a BSR-hosted dependency to validate requests, publish a module so other teams can use it, then consume your API from a client via a generated SDK.

Prerequisites

  • Buf CLI 1.36.0 or newer. Run buf --version to check.

  • git and a Go toolchain on your $PATH.

  • A Buf account.

  • The buf-examples repo cloned locally. The start directory is where you’ll make changes; finish is the reference to compare against.

    console $ git clone git@github.com:bufbuild/buf-examples.git $ cd buf-examples/bsr/quickstart/start/server

Throughout the walkthrough, replace <username> with your own BSR username.

Add a Protovalidate dependency

The quickstart’s InvoiceService has a bug: it accepts invoices with no line items. The fix is to add Protovalidate and declare a validation rule on the invoice message. Protovalidate is published as a BSR module, so you can pull it in as a dependency.

Add it to buf.yaml in the server folder:

bsr/quickstart/start/server/buf.yaml

yaml
version: v2
modules:
  - path: proto
deps:
  - buf.build/bufbuild/protovalidate
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Install it and pin a version in buf.lock:

bsr/quickstart/start/server/

sh
buf dep update
WARN    Module buf.build/bufbuild/protovalidate is declared in your buf.yaml deps but is unused.

The warning is expected. You haven’t imported Protovalidate in a .proto file yet.

Import it and add a validation rule that requires at least one line item:

bsr/quickstart/start/server/proto/invoice/v1/invoice.proto

protobuf
syntax = "proto3";

package invoice.v1;

import "buf/validate/validate.proto"; 
import "tag/v1/tag.proto";

// Invoice is a collection of goods or services sold to a customer.
message Invoice {
  string invoice_id = 1;
  string customer_id = 2;
  repeated LineItem line_items = 3; 
  repeated LineItem line_items = 3 [(buf.validate.field).repeated.min_items = 1];
}

// Code omitted for brevity

See the Protovalidate reference for the full list of rules you can use.

Generate code and test the fix

The server’s code-generation config lives in buf.gen.yaml. It’s already set up to emit Go code with a go_package_prefix option applied via managed mode.

Managed mode rewrites Protobuf file options for every .proto file the CLI compiles, which includes your dependencies. That’s a problem for Protovalidate: its package option would get overwritten, and the generated Go import path would be wrong. Fix it by disabling the go_package override for that one dependency:

bsr/quickstart/start/server/buf.gen.yaml

yaml
// Code omitted for brevity

managed:
  enabled: true
  override:
    - file_option: go_package_prefix
      value: github.com/bufbuild/buf-examples/bsr/quickstart/server/gen
disable:
  - file_option: go_package
    module: buf.build/bufbuild/protovalidate

Generate code:

bsr/quickstart/start/server/

sh
buf generate

A gen directory appears with the generated Go source.

Start the server, which imports the generated stubs:

bsr/quickstart/start/server/

sh
go run cmd/main.go
... Listening on localhost:8080

In a new terminal, try a request with no line items:

bsr/quickstart/start/server/

sh
buf curl \
    --data '{ "invoice": { "customer_id": "fake-customer-id" }}' \
    --schema . \
    --http2-prior-knowledge \
    http://localhost:8080/invoice.v1.InvoiceService/CreateInvoice
{
   "code": "invalid_argument",
   "message": "validation error:\n - invoice.line_items: value must contain at least 1 item(s) [repeated.min_items]",
   "details":
   // Response omitted for brevity
}

The validation rule rejected the bad request. The server’s cmd/main.go wires Protovalidate in as a Connect interceptor, which is how the validation runs automatically on every request. In your own services you’d add the same interceptor; see the Protovalidate documentation for specifics.

Now try a good request:

bsr/quickstart/start/server/

sh
buf curl \
    --data '{ "invoice": { "customer_id": "bob", "line_items": [{"unit_price": "999", "quantity": "2"}] }, "tags": { "tag": ["spring-promo","valued-customer"] } }' \
    --schema . \
    --http2-prior-knowledge \
    http://localhost:8080/invoice.v1.InvoiceService/CreateInvoice
{}

Back in the server’s terminal, you’ll see the invoice being created:

bsr/quickstart/start/server/

text
2025/03/04 17:21:59 Creating invoice for customer bob for 1998
2025/03/04 17:21:59   - spring-promo
2025/03/04 17:21:59   - valued-customer

Stop the server with Ctrl-c.

The request includes tags that business analysts use to categorize invoices, and the response echoes them back. Other teams at your company could use the same tag types for reporting, campaigns, and analytics. Split them into their own module so others can depend on them without pulling in the full invoice API.

Publish a shared module

Create an organization to own the new module:

sh
buf registry login
buf registry organization create buf.build/<username>-quickstart
Created buf.build/<username>-quickstart

buf registry login opens a browser, asks you to approve the token, and writes the credentials to .netrc. The walkthrough requires an organization; personal user namespaces can’t host BSR modules for this flow.

Create a common repository in the new organization:

sh
buf registry module create buf.build/<username>-quickstart/common --visibility public
Created buf.build/<username>-quickstart/common.

Create a sibling directory for the module alongside server:

bsr/quickstart/start/server/

sh
cd ..
mkdir common && cd common

Initialize the module:

bsr/quickstart/start/common/

sh
buf config init

Wire up buf.yaml with the module path and BSR name:

bsr/quickstart/start/common/buf.yaml

yaml
version: v2
modules:
  - path: proto
    name: buf.build/<username>-quickstart/common
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Move tag.proto out of the server into the new module:

bsr/quickstart/start/common

sh
mkdir proto && mv ../server/proto/tag/ ./proto

Build to confirm there are no errors (silent output means success):

bsr/quickstart/start/common

sh
buf build

Push the module:

bsr/quickstart/start/common

sh
buf push
buf.build/<username>-quickstart/common:e1fb01dc1bac43ad9b8ca03b7911834c

The commit ID is how other modules will pin to this version.

Add a README so consumers of the module know what it’s for:

bsr/quickstart/start/common/README.md

text
# Tags

This module allows you to add custom tags for tracking or analysis.

Push again to upload the README:

bsr/quickstart/start/common

sh
buf push

Visit https://buf.build/<username>-quickstart/common to see your module’s page. The BSR auto-generates Protobuf schema documentation from your .proto files and pairs it with the README. For more on authoring module docs, see Schema documentation.

Depend on the shared module

Back in server/, refactor to use the new dependency instead of the local tag/ files.

Add it to buf.yaml:

bsr/quickstart/start/server/buf.yaml

yaml
version: v2
modules:
  - path: proto
deps:
  - buf.build/bufbuild/protovalidate
  - buf.build/<username>-quickstart/common
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Update dependencies:

sh
buf dep update

buf.lock now includes the new dependency, pinned to the commit you just pushed:

bsr/quickstart/start/server/buf.lock

yaml
# Generated by buf. DO NOT EDIT.
version: v2
deps:
  - name: buf.build/bufbuild/protovalidate
    commit: d39267d9df8f4053bbac6b956a23169f
    digest: b5:c2542c2e9935dd9a7f12ef79f76aa5b53cf1c8312d720da54e03953f27ad952e2b439cbced06e3b4069e466bd9b64019cf9f687243ad51aa5dc2b5f364fac71e
  - name: buf.build/<username>-quickstart/common
    commit: ee6cb9c90d16495f82d419d9262dbd27
    digest: b5:ef7a05bd56d547893a8a2bceaf77860e7051b282120c4f0ed59bc974acf2f57f246e71a691eff52eb069659d6710572baeed26e9e38bb2111318422775805685

Regenerate Go code, update Go module dependencies, and restart the server:

bsr/quickstart/start/server/

sh
buf generate
go mod tidy
go run cmd/main.go

In the other terminal, retry the good request:

sh
buf curl \
    --data '{ "invoice": { "customer_id": "bob", "line_items": [{"unit_price": "999", "quantity": "2"}] }, "tags": { "tag": ["spring-promo","valued-customer"] } }' \
    --schema . \
    --http2-prior-knowledge \
    http://localhost:8080/invoice.v1.InvoiceService/CreateInvoice
{}

The server works the same way from the outside, but its tags API now comes from a shared module that any team can depend on.

Consume the API with a generated SDK

Every BSR module gets a set of generated SDKs for the languages the BSR supports. A team consuming your API imports the SDK through their normal package manager instead of running buf generate against your .proto files.

Publish the invoice module

The invoice service isn’t published yet. Publish it the same way you published common.

Create the BSR repository:

bsr/quickstart/start/server/

sh
buf registry module create buf.build/<username>-quickstart/invoice --visibility public
Created buf.build/<username>-quickstart/invoice.

Set the module name in buf.yaml:

bsr/quickstart/start/server/buf.yaml

yaml
version: v2
modules:
  - path: proto
    name: buf.build/<username>-quickstart/invoice
deps:
  - buf.build/bufbuild/protovalidate
  - buf.build/<username>-quickstart/common
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Push:

bsr/quickstart/start/server

sh
buf push
buf.build/<username>-quickstart/invoice:e1fb01dc1bac43ad9b8ca03b7911834c

Call the API from a client

Switch to the start/client directory, which has an empty Go module ready for the client implementation. Install the generated SDKs with go get:

bsr/quickstart/start/client/

sh
# Base Protobuf types
go get buf.build/gen/go/<username>-quickstart/invoice/protocolbuffers/go
# The generated invoice SDK
go get buf.build/gen/go/<username>-quickstart/invoice/connectrpc/gosimple

The BSR generates SDKs on first request, so the first go get for a new module takes a few seconds. Subsequent requests are instant.

Replace client/cmd/main.go with the reference implementation:

bsr/quickstart/start/client/cmd/main.go

go
package main

import (
    tagv1 "buf.build/gen/go/xUSERNAMEx-quickstart/common/protocolbuffers/go/tag/v1"
    "buf.build/gen/go/xUSERNAMEx-quickstart/invoice/connectrpc/gosimple/invoice/v1/invoicev1connect"
    invoicev1 "buf.build/gen/go/xUSERNAMEx-quickstart/invoice/protocolbuffers/go/invoice/v1"
    "context"
    "log"
    "net/http"
)

func main() {
    client := invoicev1connect.NewInvoiceServiceClient(
        http.DefaultClient,
        "http://localhost:8080",
    )

    _, err := client.CreateInvoice(
        context.Background(),
        &invoicev1.CreateInvoiceRequest{
            Invoice: &invoicev1.Invoice{
                InvoiceId:  "invoice-one",
                CustomerId: "customer-one",
                LineItems: []*invoicev1.LineItem{
                    {
                        UnitPrice: 999,
                        Quantity:  2,
                    },
                },
            },
            Tags: &tagv1.Tags{
                Tag: []string{
                    "bogo-campaign",
                    "valued-customer",
                },
            },
        },
    )
    if err != nil {
        log.Fatalf("error creating valid invoice: %v", err)
    }
    log.Println("Valid invoice created")
}

With the server still running, build and run the client:

bsr/quickstart/start/client/

sh
go mod tidy
go run cmd/main.go
2025/03/20 09:58:03 Valid invoice created

The server logs the request:

bsr/quickstart/start/server/

text
2025/03/20 09:58:03 Creating invoice for customer customer-one for 1998
2025/03/20 09:58:03   - bogo-campaign
2025/03/20 09:58:03   - valued-customer

The client never ran buf generate. It pulled strongly-typed Go bindings for your API directly from the BSR.

What’s next