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
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/
$ 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
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
// 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/
$ buf generate

A gen directory appears with the generated Go source.

Start the server, which imports the generated stubs:

bsr/quickstart/start/server/
$ go run cmd/main.go
... Listening on localhost:8080

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

bsr/quickstart/start/server/
$ 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/
$ 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/
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:

$ 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:

$ 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/
$ cd ..
$ mkdir common && cd common

Initialize the module:

bsr/quickstart/start/common/
$ buf config init

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

bsr/quickstart/start/common/buf.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
$ mkdir proto && mv ../server/proto/tag/ ./proto

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

bsr/quickstart/start/common
$ buf build

Push the module:

bsr/quickstart/start/common
$ 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
# Tags

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

Push again to upload the README:

bsr/quickstart/start/common
$ 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
version: v2
modules:
  - path: proto
deps:
  - buf.build/bufbuild/protovalidate
+ - buf.build/<username>-quickstart/common
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

Update dependencies:

$ buf dep update

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

bsr/quickstart/start/server/buf.lock
# 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/
$ buf generate
$ go mod tidy
$ go run cmd/main.go

In the other terminal, retry the good request:

$ 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/
$ 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
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
$ 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/
# 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
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/
$ 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/
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#