Skip to main content

Getting started

Connect is a slim library for building browser- and gRPC-compatible HTTP APIs. You define your service with a Protocol Buffer schema, and Connect generates type-safe server and client code. Fill in your server's business logic and you're done — no hand-written marshaling, routing, or client code required!

This fifteen-minute walkthrough helps you create a small Connect service in Go. It demonstrates what you'll be writing by hand, what Connect generates for you, and how to call your new API.

Prerequisites

Install tools

First, we'll need to create a new Go module and install some code generation tools:

$ mkdir connect-go-example
$ cd connect-go-example
$ go mod init example
$ go install github.com/bufbuild/buf/cmd/buf@latest
$ go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
$ go install connectrpc.com/connect/cmd/protoc-gen-connect-go@latest

You'll need buf, protoc-gen-go and protoc-gen-connect-go on your PATH. If which buf protoc-gen-go protoc-gen-connect-go doesn't succeed, add Go's install directories to your path:

$ [ -n "$(go env GOBIN)" ] && export PATH="$(go env GOBIN):${PATH}"
$ [ -n "$(go env GOPATH)" ] && export PATH="$(go env GOPATH)/bin:${PATH}"

Define a service

Now we're ready to write the Protocol Buffer schema that defines our service. In your shell,

$ mkdir -p greet/v1
$ touch greet/v1/greet.proto

Open greet/v1/greet.proto in your editor and add:

syntax = "proto3";

package greet.v1;

import "buf/validate/validate.proto";

message GreetRequest {
string name = 1 [(buf.validate.field).string = {
min_len: 1,
max_len: 50,
}];
}

message GreetResponse {
string greeting = 1;
}

service GreetService {
rpc Greet(GreetRequest) returns (GreetResponse) {}
}

This file declares the greet.v1 Protobuf package, a service called GreetService, a single method called Greet with request and response messages, and includes validation for GreetRequest. These package, service, and method names will reappear soon in our HTTP API's URLs.

Generate code

We're going to generate our code using Buf, a modern replacement for Google's protobuf compiler. We installed Buf earlier, but we also need a few configuration files to get going. (If you'd prefer, you can use protoc instead — protoc-gen-connect-go behaves like any other plugin.)

First, scaffold a basic buf.yaml by running buf config init and then replacing its contents with this:

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

Next, tell Buf how to generate code by putting this into buf.gen.yaml:

version: v2
plugins:
- local: protoc-gen-go
out: gen
opt: paths=source_relative
- local: protoc-gen-connect-go
out: gen
opt:
- paths=source_relative
- simple
managed:
enabled: true
override:
- file_option: go_package_prefix
value: example/gen
disable:
- file_option: go_package
module: buf.build/bufbuild/protovalidate

With those configuration files in place, you can lint your schema and generate code:

$ buf dep update
$ buf lint
$ buf generate

In your gen directory, you should now see some generated Go:

gen
└── greet
└── v1
├── greet.pb.go
└── greetv1connect
└── greet.connect.go

The package gen/greet/v1 contains greet.pb.go, which was generated by Google's protoc-gen-go, and it contains the GreetRequest and GreetResponse structs and the associated marshaling code. The package gen/greet/v1/greetv1connect contains greet.connect.go, which was generated by protoc-gen-connect-go, and it contains the HTTP handler and client interfaces and constructors. Feel free to poke around if you're interested — greet.connect.go is just over 100 lines of code, including comments.

Implement handler

The code we've generated takes care of the boring boilerplate, but we still need to implement our greeting logic. In the generated code, this is represented as the greetv1connect.GreetServiceHandler interface. Since the interface is so small, we can do everything in one Go package. mkdir -p cmd/server, then add cmd/server/main.go:

package main

import (
"context"
"fmt"
"net/http"

greetv1 "example/gen/greet/v1" // generated by protoc-gen-go
"example/gen/greet/v1/greetv1connect" // generated by protoc-gen-connect-go

"connectrpc.com/connect"
"connectrpc.com/validate"
)

type GreetServer struct{}

func (s *GreetServer) Greet(
_ context.Context,
req *greetv1.GreetRequest,
) (*greetv1.GreetResponse, error) {
res := &greetv1.GreetResponse{
Greeting: fmt.Sprintf("Hello, %s!", req.Name),
}
return res, nil
}

func main() {
greeter := &GreetServer{}
mux := http.NewServeMux()
path, handler := greetv1connect.NewGreetServiceHandler(
greeter,
// Validation via Protovalidate is almost always recommended
connect.WithInterceptors(validate.NewInterceptor()),
)
mux.Handle(path, handler)
p := new(http.Protocols)
p.SetHTTP1(true)
// Use h2c so we can serve HTTP/2 without TLS.
p.SetUnencryptedHTTP2(true)
s := http.Server{
Addr: "localhost:8080",
Handler: mux,
Protocols: p,
}
s.ListenAndServe()
}

In a separate terminal window, you can now update go.mod and start your server:

$ go get connectrpc.com/connect
$ go get connectrpc.com/validate
$ go get buf.build/go/protovalidate
$ go run ./cmd/server/main.go

Make requests

The simplest way to consume your new API is an HTTP/1.1 POST with a JSON payload:

$ curl \
--header "Content-Type: application/json" \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet

This responds:

{"greeting": "Hello, Jane!"}

Your new handler automatically supports gRPC requests, too:

$ buf curl \
--schema ./greet/v1/greet.proto \
--protocol grpc \
--http2-prior-knowledge \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet

This responds:

{
"greeting": "Hello, Jane!"
}

Or, with the Connect protocol:

$ buf curl \
--schema . \
--data '{"name": "Jane"}' \
http://localhost:8080/greet.v1.GreetService/Greet

We can also make requests using Connect's generated client. mkdir -p cmd/client and put this in cmd/client/main.go:

package main

import (
"context"
"log"
"net/http"

greetv1 "example/gen/greet/v1"
"example/gen/greet/v1/greetv1connect"
)

func main() {
client := greetv1connect.NewGreetServiceClient(
http.DefaultClient,
"http://localhost:8080",
)
res, err := client.Greet(
context.Background(),
&greetv1.GreetRequest{Name: "Jane"},
)
if err != nil {
log.Println(err)
return
}
log.Println(res.Greeting)
}

With your server still running in a separate terminal window, you can now run your client:

$ go run ./cmd/client/main.go

Now try sending a request with no data:

$ buf curl \
--schema . \
http://localhost:8080/greet.v1.GreetService/Greet

It's rejected because you've included Protovalidate rules in your schema:

{
"code": "invalid_argument",
"message": "validation error:\n - name: value length must be at least 1 characters [string.min_len]"
}

Congratulations — you've built your first Connect service! 🎉

Use the gRPC protocol instead of the Connect protocol

connect-go supports three protocols:

  • The new Connect protocol, a simple, HTTP-based protocol that works over HTTP/1.1 or HTTP/2. It takes the best portions of gRPC and gRPC-Web, including streaming, and packages them into a protocol that works equally well in browsers, monoliths, and microservices. The Connect protocol is what we think the gRPC protocol should be. By default, JSON- and binary-encoded Protobuf is supported.
  • The gRPC protocol that is used throughout the gRPC ecosystem, making connect-go compatible with other gRPC implementations out of the box. grpc-go clients will work with connect-go servers and vice versa without issue - in fact, before connect-go's public release, this is exactly what the Buf CLI was doing.
  • The gRPC-Web protocol used by grpc/grpc-web, allowing connect-go servers to interop with grpc-web frontends without the need for an intermediary proxy (such as Envoy).

By default, connect-go servers support ingress from all three protocols without any configuration. connect-go clients use the Connect protocol by default, but can use either the gRPC or gRPC-Web protocols by setting the WithGRPC or WithGRPCWeb client options.

Edit cmd/client/main.go above to create the GreetServiceClient using the WithGRPC option:

client := greetv1connect.NewGreetServiceClient(
http.DefaultClient,
"http://localhost:8080",
connect.WithGRPC(),
)

With your server still running in a separate terminal window, run the client one more time:

$ go run ./cmd/client/main.go

Your output should remain the same, but connect-go is now using the gRPC protocol instead of the Connect protocol to communicate over the wire.

So what?

With just a few lines of hand-written code, you've built a real API server that supports both the gRPC and Connect protocols. Unlike a hand-written REST service, you didn't need to design a URL hierarchy, hand-write request and response structs, manage your own marshaling, or parse typed values out of query parameters. More importantly, your users got an idiomatic, type-safe client without any extra work on your part.