Zenn
💫

ConnectによるgRPCを超えたスキーマ駆動開発(Golang/TypeScript)

2025/01/27に公開
31

こんにちは。PortalKeyの植森です。

前回、PortalKeyの主要技術に関してざっくりと解説をしました。
今回は、その中のひとつであるConnectについて掘り下げていきます。

Connectとは?

https://connectrpc.com/

Connectは、Buf Technologies社が開発したRPCフレームワークです。Protocol Buffersによって通信を定義し、効率的なサーバー/クライアント間通信を実現します。

まずはConnectの特徴について解説していきます。

Production-grade simplicity: 本番運用のシンプルさ

Connectは本番運用でのシンプルさを特徴の1つとしています。
gRPCは通信するためにgRPCクライアントや grpcurl のようなツールを使わなければ通信することが困難であったり、Webから利用する場合 gRPC-Web を使う必要がありそのために Envoy などの専用のプロキシが必要などのデメリットがありました。

ConnectはConnect Protocolという独自のプロトコルを使うことができ、このプロトコルは広く利用されているHTTP機能にのみ依存して実装されています。
Connect Protocolは以下のような特徴を持ちます。

  • HTTP Semanticsに準拠して動作し、既存のネットワークライブラリを広く利用することが出来る
  • Unary通信の場合、 HTTPツールを使用してデバッグが可能
  • Protocol Buffersスキーマと併用することで、Protobuf BinaryまたはJSONペイロードのいずれかを利用してメッセージのやり取りが可能
  • 双方向ストリーミングは HTTP/2 が必要だが、それ以外のRPCタイプは HTTP/1.1 をサポート

gRPCを使っている場合gRPCのエコシステムに依存することになりますが、Connectを利用することで汎用的なHTTPツールを利用できるのは大きなメリットです。
例えばConnectではcurlとJSONを使って簡単な通信のテストをし、ChromeのDeveloperツールをそのまま使って通信内容をデバッグし、locustをカスタマイズなしで利用して負荷試験を行うことが可能です。

Compatible with gRPC: gRPCとの互換性

ConnectはConnect独自のProtocolでの通信が可能ですが、それ以外にgRPCおよびgRPC-Webとの互換性を持っており、gRPC、gRPC-Web、Connect Protocolの3つのプロトコルをサポートしています。

https://connectrpc.com/docs/protocol/

どのぐらいの互換性があるかというとConnectサーバはgRPCクライアント、ConnectクライアントはgRPCサーバと直接通信することが可能で、公式ではStreaming, Trailer, Error Detailなどを含んだgRPCプロトコルの完全なサポートを謳っています。
※Connect-Webを利用している(HTTP/1.1を利用している)場合のストリーミング通信はWebSocketの通信にフォールバックされます。

これによってgrpcurlやgRPC GatewayといったgRPC実装とシームレスに連携が可能で、gRPCのエコシステムを利用することが可能になっています。
また、Connectの大きな特徴の一つとしてEnvoyのようなプロキシに依存せずにgRPC-Webプロトコルとネイティブに通信することが出来るのは大きなメリットの1つとなるでしょう。

Familiar primitives: 各言語のプリミティブな実装への対応

Connectは各言語の実装においてもプリミティブな実装の上に構築されています。

例えばGolangにおいては、 net/http パッケージに準拠しており、 net/http を利用したサーバにそのまま組み込むことが出来ます。
これはgRPCサーバが net/http との互換がないことへの比較になっています。

Connectが net/http のエコシステムに則っていることで以下のようなメリットがあります。

  • net/http のライブラリやエコシステム、知識をそのまま使うことが出来る
  • 他のサーバ、例えば通常のHTTPサーバやWebSocketサーバと同居する際にメトリクスの監視やロギング、認証・認可といった処理を透過的に単一のHTTPミドルウェアによって処理することが出来る
  • テストを専用のライブラリを使わず通常の net/http と同じように書くことが出来る

新しいアーキテクチャはインターフェースや挙動を理解するのに時間がかかり、また既存の知識やライブラリが流用できずインピーダンスミスマッチに苦しむことも多いです。
Connectは既存のエコシステムを尊重した設計となっており、Connect独自の何かに苦しむことはほとんどありません
同様にTypeScriptではfetch APIに近い設計になっており、一般的なUIフレームワークにもスムーズに統合することが出来ます。

No boilerplate: ボイラープレート不要

ConnectはProtocol Buffersを前提とした設計をされており、Protocol Buffersを利用することでボイラープレートコードを書く必要はありません
サーバおよびクライアントのルーティング/シリアライズ/圧縮といった処理はProtocol Buffers Pluginを通じて各言語の実装を生成します。

Connectを提供するBuf Technologies IncはBufというProtocol Buffers向けツールを開発しており、Protocol Buffersに対するエコシステムの開発にも積極的です。

gRPCを利用するメリットには様々な側面があります。 HTTP/2 による高速化やストリーミング通信などは最たるものですが、個人的にはより広くインパクトを与えたのはスキーマ定義によるクロスプラットフォームなやり取りをシームレスに可能になった点だと考えています。
APIによる各プラットフォーム・サービスの連携が当たり前になり、マイクロサービスが登場したことでサービス間の連携や言語間の実装の互換性が課題となった時代でgRPCのスキーマ駆動開発が着目されましたが、gRPCには上で述べたように様々な問題点がありました。

Connectはスキーマ駆動開発のメリットを最大限に活かしながら、HTTPセマンティクスや各言語のプリミティブな設計、およびgRPCとの互換性を保つことでそういった問題をクリアすることを目指しているのだと思います。

Connectの採用理由

Connectの採用理由ですがここまでConnectの説明をしてきたので明白だろうと思います。

  • Protocol Buffersの思想が非常に優れており、IDLとして採用を決めていた
  • gRPCのメリットの1つであるスキーマ駆動開発のスタートアップ速度を享受しつつ、gRPCに依存することで起きる煩わしさからの解放
  • net/http 実装のWebSocketサーバやHTTPサーバと同一サーバでシームレスに同居可能

まず、スキーマ定義に利用する言語としてProtocol Buffersを採用することは最初から決めていました。
少人数で複数言語を扱いながら効率よく開発する上でIDLおよびコードの自動生成は外せないポイントです。
他の選択肢としてサーバとクライアントを同一言語(つまりTypeScript)で実装するなどの方法もありましたが、メンバーの興味関心や自身の経験を踏まえ、サーバをGolangで開発することに決めました。

Protocol Buffersは独自の記法によるシンプルながらもPluginによる拡張性があり非常に手に馴染む道具です。
Protocol Buffersはスキーマ定義言語であり、通信のためだけではなく例えばBigQueryのスキーマ定義やログスキーマの定義など幅広いスキーマを定義可能な柔軟さから用途が非常に豊富なため自分の中では積極的に採用するツールの1つになっています。

Protocol Buffersでコード生成を行うにあたりgRPCを利用するかを考えましたが、gRPCの実装の際にgRPC独自の仕様やライブラリ・ツールの選択の幅の狭さに苦しめられた記憶がありました。
また、前回はAPIサーバとしてでの利用であり、今回はさらにgRPC-Webが必要なこと、またサーバ開発経験がゼロのメンバーがいることも考えるとあまり良い選択肢ではないと思い、gRPCを使うぐらいならプラグインを自作してHTTP/1.1で通信するためのコードを生成する方が良いと考えていました。

ここには前述したとおり、gRPCに求めていたのはスキーマの自動生成とその開発開始速度であり、コード生成プラグインの実装が数日程度で終わるであろうことを考えるとデメリットの方が目立つというのがgRPC不採用の大きな理由です。

そんな中、Protocol Buffers周りの技術を調べているときにConnectのことを知り、調査したところユースケースに非常にマッチしているということで採用を決めました。
Connectを採用したことを振り返ると、自身がgRPCの知識があったというのもありますが、独自の仕様に振り回されることはほとんどなく、汎用的な知識を活かして開発できるというのは非常に快適で採用して良かったと思っています。

また、今回はWebSocketサーバやHTTPサーバも同時に実装する必要があることは設計時からわかっており、それらのサーバとシームレスに同居することが出来るのも大きなメリットの1つとなりました。
gRPCの場合もnet/httpのハンドラと同一サーバで動かすことは可能ですが、少し違和感のある動かし方になり、またHTTPミドルウェアとの親和性もないため、完全に別な物を無理矢理1つにして動かしているような感覚でした。
Connectは net/http 準拠ということもあり、違和感なく自然に動作させることが出来る点もよかったと思っています。

1つだけ難点を上げるとすると、去年の夏ぐらいにv1からv2へのbreaking changeを含む大きなアップグレードがあり、クライアント側のコードが壊れていまだにv2に上げれていないことが気がかりです。
今から始める皆さんは必ずv2で始めましょう。

実践Connect

最後に実践編ということで、GolangとTypeScriptで通信するサンプルコードを載せておきます。
ゴールは以下の通りです。

  • クライアントはサーバに Greet({ name: name }) のようにメッセージを飛ばし、返ってきたメッセージを表示する
  • curlやgrpcurlで通信で出来ることをテストする
  • Golangで様々なサーバのテストを書く

こちらで書いたコードは以下のリポジトリにあるのですべてのコードを確認したい方はリポジトリを参照してください。

https://github.com/yuemori/connect-sample

また、このサンプルコードはConnect-GoのGetting Startedをベースにしています。
https://connectrpc.com/docs/go/getting-started/

セットアップ

まずはセットアップとして、関連ツールのインストールを行います。
ここではProtocol Buffersのコード生成にはBufを利用し、GolangとConnectのコードを生成するためにproto pluginのインストールを行っています。

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

インストールが完了したら、bufコマンドを使って設定ファイルの初期化を行います。
コマンドを実行するとカレントディレクトリにbuf.yamlが生成されます。

buf config init

ここでは詳細な説明を省きますが、bufはlintやスキーマチェックなどの機能も持っており様々な設定が行えて非常に便利です。
興味のある人はbufのドキュメントを読んでください。

buf.yaml
# For details on buf.yaml configuration, visit https://buf.build/docs/configuration/v2/buf-yaml
version: v2
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

protoファイルの作成

次にサンプル用のprotoファイルを作成します。
今回は Greet というメソッドを持つ GreetService を定義し、名前を送ると挨拶が返ってくるようにします。

mkdir -p greet/v1
touch greet/v1/greet.proto
greet.proto
syntax = "proto3";

package greet.v1;

option go_package = "example/gen/greet/v1;greetv1";

message GreetRequest {
  string name = 1;
}

message GreetResponse {
  string greeting = 1;
}

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

次に、buf.gen.yamlを追加します。
このファイルはprotoファイルからコード生成を行うための設定ファイルです。
ここに利用するpluginと出力先、オプションなどを指定します。

今回はGolangのコードを生成するprotoc-gen-goと、connect-goのコードを生成するprotoc-gen-connect-goを追加します。

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

buf.gen.yamlを追加したら、buf generateを実行することでpb.goとconnect.goが自動生成されます。

buf generate
gen/greet/v1/greet.pb.go
gen/greet/v1/greetv1connect/greet.connect.go

生成されたgreet.connect.goにはGreetServiceHandlerのinterface定義があります。
Connectでは、このinterfaceを満たすようにHandlerの実装を行います。

greet.connect.go
// GreetServiceHandler is an implementation of the greet.v1.GreetService service.
type GreetServiceHandler interface {
	Greet(context.Context, *connect.Request[v1.GreetRequest]) (*connect.Response[v1.GreetResponse], error)
}

生成されたファイルの詳細はこちらからどうぞ。
https://github.com/yuemori/connect-sample/blob/main/gen/greet/v1/greetv1connect/greet.connect.go

サーバの実装

次にサーバの実装を行います。

mkdir -p cmd/server

ポイントとしては greetv1connect.NewGreetServiceHandler(greeter) の部分で、greeterに先ほどのinterface定義を満たすHandlerを渡すことでこのServiceがリクエストを待ち受けるパスと http.Handler が返却されます。

あとはこのpathと http.Handler をHTTP Serverにそのまま登録することでエンドポイントとして機能するようになります。

cmd/server/main.go
package main

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

	"connectrpc.com/connect"
	"golang.org/x/net/http2"
	"golang.org/x/net/http2/h2c"

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

type GreetServer struct{}

func (s *GreetServer) Greet(
	ctx context.Context,
	req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
	log.Println("Request headers: ", req.Header())
	res := connect.NewResponse(&greetv1.GreetResponse{
		Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
	})
	res.Header().Set("Greet-Version", "v1")
	return res, nil
}

func main() {
	greeter := &GreetServer{}
	mux := http.NewServeMux()
	path, handler := greetv1connect.NewGreetServiceHandler(greeter)
	mux.Handle(path, handler)
	http.ListenAndServe(
		"localhost:8080",
		// Use h2c so we can serve HTTP/2 without TLS.
		h2c.NewHandler(mux, &http2.Server{}),
	)
}

curlとgrpcurlでの動作確認

実際にサーバを動かして、curlとgrpcurl両方からのリクエストが受け取れることを確認します。

go get golang.org/x/net/http2
go get connectrpc.com/connect
go run ./cmd/server/main.go

まずはcurlで確認します。
Connectサーバは application/json もしくは application/proto のContent-Typeを受け付けます。
JSONでのリクエストを行いたいときは、 application/json を指定することでシンプルにJSONでのリクエストを送ることが出来ます。

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

次にgrpcurlで確認します。

grpcurl -protoset <(buf build -o -) -plaintext -d '{"name": "Jane"}' localhost:8080 greet.v1.GreetService/Greet
{
  "greeting": "Hello, Jane!"
}

このように、Connectのサーバは特別なミドルウェアやヘッダなどを一切使わずともHTTPおよびgRPCのツールを利用することが出来ます。
これが可能であるということはその他様々なツールやミドルウェアとの連携もシンプルに行えると考えてよいでしょう。

フロントエンドのセットアップ

次にConnect-Webを利用したフロントエンドの実装を行います。
セットアップはConnect-WebのGetting Startedから引っ張ってきています。
https://connectrpc.com/docs/web/getting-started/

ここではViteのテンプレートを使ってセットアップを行い、bufbuildのライブラリのインストールを行っています。

npm create vite@latest -- frontend --template react-ts
npm install
npm install --save-dev @bufbuild/buf @bufbuild/protoc-gen-es
npm install @connectrpc/connect @connectrpc/connect-web @bufbuild/protobuf

次に、protoc-gen-esをbufから利用するためにPATHを通します。
bufおよびprotocはpluginを利用する際にPATHからpluginを探索するため、PATHを通す必要があります。
protoc-gen-esはnode_modules以下にインストールされているため、node_modules/.binにPATHを通します。

export PATH=$PATH:$(pwd)/node_modules/.bin
which protoc-gen-es

次に、buf.gen.yamlにprotoc-gen-esの設定を追加します。
今回はフロントとサーバをモノレポで実装していますが、リポジトリを分ける場合はprotoファイルを共有したうえでそれぞれのリポジトリで必要な場所に出力するように設定すればOKです。

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
+ # This will invoke protoc-gen-es and write output to src/gen
+ - local: protoc-gen-es
+   out: frontend/src/gen
+   # Also generate any imported dependencies
+   include_imports: true
+   # Add more plugin options here
+   opt: target=ts

buf generateを行ってコードを生成します。

buf generate

生成されたコードは以下のファイルです。

https://github.com/yuemori/connect-sample/blob/main/frontend/src/gen/greet/v1/greet_pb.ts

フロントエンドの実装

今回はInputフォームに入力した名前をボタンを押すとサーバに送信し、返ってきた文字列をそのまま表示するだけのページを作成します。

まず最初に以下のようなフォームを作りました。

frontend/src/App.tsx
import { useState } from "react";
import "./App.css";

function App() {
  const [inputValue, setInputValue] = useState<string>("");

  return (
    <>
      <form>
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          type="text"
        />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

export default App;

次に、Connect Clientを作成します。
この時、作成したクライアントに利用するServiceを登録します。
ここのGreetServiceは先ほど自動生成されたgreet_pbからimportできます。

src/App.tsx
import { useState } from "react";
import "./App.css";

+import { createClient } from "@connectrpc/connect";
+import { createConnectTransport } from "@connectrpc/connect-web";
+import { GreetService } from "./gen/greet/v1/greet_pb";

+const transport = createConnectTransport({
+  baseUrl: "http://localhost:8080",
+});

+const client = createClient(GreetService, transport);
function App() {

clientに対し、 greet({ name: name }) を呼び出します。
protoc-gen-esはTypeScriptのコードを生成するため、型安全にリクエストとレスポンスを扱うことが出来ます。

src/App.tsx
function App() {
+  const [greeting, setGreeting] = useState<string>("");

+  const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
+    e.preventDefault();
+    setInputValue("");
+    const res = await client.greet({ name: inputValue });

+    setGreeting(res.greeting);
+  };

  return (
    <>
-      <form>
+      <form onSubmit={onSubmit}>
+        {greeting !== "" && <div>{greeting}</div>}
        <input
          value={inputValue}
          onChange={(e) => setInputValue(e.target.value)}
          type="text"
        />
        <button type="submit">Send</button>
      </form>
    </>
  );
}

CORSの設定

この後、フォームに入力するとコンソールにエラーが表示されます。

これはViteが5173ポートで動いており、8080ポートで動いているサーバに対しCORSエラーが発生しているため起こっているエラーです。

この問題を解消するために、サーバ側にCORSの設定を追加します。

go get connectrpc.com/cors
go get github.com/rs/cors
cmd/server/main.go
        "net/http"

        "connectrpc.com/connect"
+
+       connectcors "connectrpc.com/cors"
+       "github.com/rs/cors"
        "golang.org/x/net/http2"
        "golang.org/x/net/http2/h2c"

@@ -36,8 +39,22 @@ func main() {
        if err := http.ListenAndServe(
                "localhost:8080",
                // Use h2c so we can serve HTTP/2 without TLS.
-               h2c.NewHandler(mux, &http2.Server{}),
+               withCORS(h2c.NewHandler(mux, &http2.Server{})),
        ); err != nil {
                log.Fatalf("Failed to start server: %v", err)
        }
 }
+
+func withCORS(connectHandler http.Handler) http.Handler {
+       c := cors.New(cors.Options{
+               AllowedOrigins: []string{
+                       "http://localhost:5173",
+               },
+               AllowedMethods: connectcors.AllowedMethods(),
+               AllowedHeaders: connectcors.AllowedHeaders(),
+               ExposedHeaders: connectcors.ExposedHeaders(),
+               MaxAge:         7200, // 2 hours in seconds
+       })
+
+       return c.Handler(connectHandler)
+}

今回 https://github.com/rs/cors を使ってCORSの対応を行っていますが、このパッケージはhttp.Handlerに対応したライブラリです。
これは前述したとおり、Connectがプリミティブな net/http ハンドラーに準拠しているためそのエコシステムを利用することが出来ています。

go run cmd/server/main.go

サーバを再起動することで、メッセージが表示されることを確認できました。

Test

最後にテストを書いて終わりにします。
まずはcmd/main.goからhandlerのコードを分離しました。

handler/greet.to
package handler

import (
	"context"
	"fmt"
	"log"

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

	"connectrpc.com/connect"
)

type GreetServer struct{}

func (s *GreetServer) Greet(
	ctx context.Context,
	req *connect.Request[greetv1.GreetRequest],
) (*connect.Response[greetv1.GreetResponse], error) {
	log.Println("Request headers: ", req.Header())
	res := connect.NewResponse(&greetv1.GreetResponse{
		Greeting: fmt.Sprintf("Hello, %s!", req.Msg.Name),
	})
	res.Header().Set("Greet-Version", "v1")
	return res, nil
}

このサーバに対するテストを書いてみます。
ここでは以下の3パターンのテストを実装しています。

  • GreetServerのGreet()を直接呼び出すテスト
  • HTTP通信をモックしてシミュレートするテスト
  • HTTPサーバを実際に起動し、HTTP通信を送信・受信するテスト
handler/greet_test.go
package handler_test

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"strings"
	"testing"

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

	"connectrpc.com/connect"
)

func TestMain(m *testing.M) {
	os.Exit(m.Run())
}

// 直接 Greet() を呼び出すテスト
func TestGreetServer_Direct(t *testing.T) {
	s := &handler.GreetServer{}

	ctx := context.Background()

	req := connect.NewRequest(&greetv1.GreetRequest{
		Name: "Alice",
	})

	res, err := s.Greet(ctx, req)
	if err != nil {
		t.Fatalf("Greet failed: %v", err)
	}

	if res.Msg.Greeting != "Hello, Alice!" {
		t.Errorf("Greeting is incorrect: %s", res.Msg.Greeting)
	}

	if res.Header().Get("Greet-Version") != "v1" {
		t.Errorf("Greet-Version header is incorrect: %s", res.Header().Get("Greet-Version"))
	}
}

// httptest.Requestを使ってHTTP通信をシミュレートしたテスト
func TestGreetServer_WithSimulateHTTPTest(t *testing.T) {
	s := &handler.GreetServer{}
	mux := http.NewServeMux()
	path, handler := greetv1connect.NewGreetServiceHandler(s)
	mux.Handle(path, handler)

	body := strings.NewReader(`{"name":"Alice"}`)

        // httptest.Requestを使って通信をシミュレートする
	req := httptest.NewRequest("POST", "/greet.v1.GreetService/Greet", body)
	req.Header.Add("Content-Type", "application/json")
	rec := httptest.NewRecorder()

	mux.ServeHTTP(rec, req)

	if rec.Code != http.StatusOK {
		t.Fatalf("expected status 200; got %v", rec.Code)
	}

	if rec.Body.String() != "{\"greeting\":\"Hello, Alice!\"}" {
		t.Fatalf("expected body to be '{\"greeting\":\"Hello, Alice!\"}'; got %v", rec.Body.String())
	}
}

// HTTPサーバを起動し、http.Requestを使ったテスト
func TestGreetServer_WithHTTPTestServer(t *testing.T) {
	s := &handler.GreetServer{}
	mux := http.NewServeMux()
	path, handler := greetv1connect.NewGreetServiceHandler(s)
	mux.Handle(path, handler)

        // httptest.Serverを起動してテストする
	testServer := httptest.NewServer(mux)
	defer testServer.Close()

	body := strings.NewReader(`{"name":"Alice"}`)

        // リクエストは通常通りhttp.Requestを使う
	req, err := http.NewRequest("POST", testServer.URL+"/greet.v1.GreetService/Greet", body)
	if err != nil {
		t.Fatalf("failed to create request: %v", err)
	}
	req.Header.Add("Content-Type", "application/json")

	resp, err := new(http.Client).Do(req)
	if err != nil {
		t.Fatalf("failed to send request: %v", err)
	}

	respBody, err := io.ReadAll(resp.Body)
	if err != nil {
		t.Fatalf("failed to read response body: %v", err)
	}

	if resp.StatusCode != http.StatusOK {
		t.Fatalf("expected status 200; got %v", resp.StatusCode)
	}

	if string(respBody) != "{\"greeting\":\"Hello, Alice!\"}" {
		t.Fatalf("expected body to be '{\"greeting\":\"Hello, Alice!\"}'; got %v", respBody)
	}
}

実際どのパターンのテストを書くかはポリシーによって異なると思います。
単純にHandlerの挙動をテストしたいのであればServiceのテストを書けばいいし、http.Handlerやconnect.Interceptorを含めたテストを書きたいのであればE2Eテストを書けばいいでしょう。

ここでもConnectが net/http の挙動に準拠していることで、 net/httptest を利用することができています。
特別なマジックや専用のライブラリを使用することなく、標準的な方法でテストを書くことが出来るのは大きなメリットです。

まとめ

いかがだったでしょうか。
ConnectはHTTPや各言語の標準を大事にしながら、Protocol Buffersを利用した型安全かつスピード感のある開発をサポートするフレームワークです。

ここでは触れませんでしたが、Error HandlingやInterceptor、利用するProtocolの選択など様々な機能がありながらもシンプルな設計となっており、非常に使いやすいです。
何よりConnectが標準を大事にしていることで、独自仕様に悩まされず今まで培った経験や技術をきっちりと活かすことができることはとても気持ちよく、Connectを採用する大きな魅力であると言えるでしょう。
また、gRPCとも互換性があるためgRPCのエコシステムを流用することが出来たり、gRPCからの乗り換えについても一定の期待をして良いと思います。

今後のWeb開発の選択肢の1つとして採用を検討してみてはいかがでしょうか?

なお弊社では採用していませんが、双方向を含めたストリーミング通信に関してもサポートしているため興味のある方はドキュメントを読んでみると良いでしょう。
弊社でWebSocketを採用してConnectを採用していない理由についてはまた別の記事にて書く予定なので、この記事が良いと思った方はいいねやフォローをお願いします。

それでは。

31
PortalKey Tech Blog

Discussion

ログインするとコメントできます