Better gRPC with Connect: A Go Application Migration

Featured on Hashnode

Our digital cognitive behavioral therapy app Awarefy has been developing and operating its backend system using Go + gRPC / Protocol Buffers since April 2022. Due to the ongoing web app development and the need to switch to connect-go, we have migrated from grpc-go to connect-go.

What is Connect?

Connect is best understood as a Better gRPC.

Connect is a slim library for building browser- and gRPC-compatible HTTP APIs.

Connect is developed by Buf, an organization that is passionate about gRPC.

Buf has been providing tools for building and maintaining Protocol Buffers schema files and a schema registry even before Connect.

Their commitment to creating a more strict and comprehensible specification can be seen in their efforts.

Buf announced the concept of Connect in January 2022.

Buf's recent focus is on Connect, which is already at the center of development experiences using gRPC / Protocol Buffers.

What are the benefits of using Connect?

The benefits of using Connect include:

  1. Supporting gRPC, Connect, and grpc-web with a single codebase

  2. Resolving issues with gRPC when using JavaScript in a browser as a client (grpc-web)

  3. gRPC compatibility allows client code incompatible with Connect to work.

  4. Easier debugging compared to gRPC as it accepts traditional RESTful API / JSON (application/json) requests (although only for POST)

For more details, refer to the official documentation.

Support for Connect in various languages

Buf supports Go, Kotlin, Swift, Web(Client-side JavaScript), and Node.js.

These are for Connect support and other languages, Connect is not used; instead, gRPC is used for processing due to backward compatibility.

Awarefy's app is developed using Flutter, so Dart support is awaited. Advantageously, the Flutter / Dart side code does not need to be changed if it is non-Connect gRPC.

connect-go

connect-go is a library for developing client/server applications with Connect support in the Go language.

Before Connect, grpc-go was practically essential for developing gRPC web servers in the Go language.

grpc-go required adherence to grpc-go conventions, which had some differences from RESTful API development knowledge. Connect can be said to have fewer differences.

A demo app using connect-go is available.

This should give you a good sense of the overall implementation.

Development flow with Connect

The development flow with Connect is as follows:

  1. Write Protocol Buffers definitions

  2. Generate code with the Buf command

  3. Implement the backend

This development flow is no different from that of gRPC.

An example of buf.gen.yaml is as follows:

version: v1
managed:
  enabled: true
plugins:
  - name: go
    out: gen
    opt: paths=source_relative
  - name: connect-go
    out: gen
    opt: paths=source_relative

Tips for migrating from grpc-go to connect-go

Here are some tips for migrating from grpc-go to connect-go. Since the code is only shown in fragments, please compare it with the demo app mentioned earlier and proceed.

An official migration guide is also available.

HTTP Server

This might be the most significant difference.

s := grpc.NewServer()

The part that depended on grpc-go is no longer needed, and the code is changed to use http.Server.

mux := http.NewServeMux()

srv := &http.Server{
    Addr: fmt.Sprintf(":%v", port),
    Handler: h2c.NewHandler(
        mux,
        &http2.Server{},
    ),
}

in this sense, switching to Connect brings the development closer to standard web app development.

Request / Response

For request and response handling, simply wrap the request with connect.Request and the response with connect.Response. This can be done with a simple find and replace.

type healthCheckController struct{}

func NewHealthCheckServiceServer() svc.HealthCheckServiceHandler {
    return &healthCheckController{}
}

func (h healthCheckController) Check(
    context.Context,
    *connect.Request[pb.HealthCheckRequest],
) (
    *connect.Response[pb.HealthCheckResponse],
    error,
) {
    return connect.NewResponse(&pb.HealthCheckResponse{}), nil
}

Note: The code starts with svc. and pb. are imports from files automatically generated by the buf command.

Error Codes As mentioned earlier, since Connect is gRPC-compatible, you can use the google.golang.org/grpc/status library for expressing errors in gRPC.

However, it is better to switch to Connect's error-related features, as they are almost mechanically interchangeable.

connect.NewError(connect.CodeUnauthenticated, errors.New("failed to get a token"))

Interceptor (Middleware)

Interceptor (Middleware) is another area with significant changes.

The example below is a logging interceptor.

func NewLoggingInterceptor() connect.UnaryInterceptorFunc {
    interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
        return connect.UnaryFunc(func(
            ctx context.Context,
            req connect.AnyRequest,
        ) (connect.AnyResponse, error) {
            Logger.Info(
                "Request",
                zap.String("Procedure", req.Spec().Procedure),
                zap.String("Protocol", req.Peer().Protocol),
                zap.String("Addr", req.Peer().Addr),
            )
            return next(ctx, req)
        })
    }
    return connect.UnaryInterceptorFunc(interceptor)
}

The code above next() processes the request, and the code below processes the response.

Interceptors need to be registered and used.

mux := http.NewServeMux()
mux.Handle(cg.NewHealthCheckServiceHandler(
    controller.NewHealthCheckServiceServer(),
    connect.WithInterceptors(
        interceptor.NewLoggingInterceptor(),
    ),
))

Wrapping Up

The development experience with Connect is truly amazing, so we highly recommend giving it a try. In the future, we plan to share the validation results of Connect-Web, so stay tuned.