😸

Connectに入門してみた

に公開

はじめに

この記事はTechBull Advent Calendar 2025 の 22日目の記事になります。

TechBullについては、TechBullのボスであられるadachin-sanの書いた「TechBullとは」を見てください。

雑に遷移しなくても一言で書いてあるので引用しておきます。

TechBullはSRE @adachin0817が創業したジュニアエンジニアの成長を支援するエンジニアコミュニティです。

本記事は、

  • 最近あたらしい技術のキャッチアップができていなかったこと
  • ちょうどConnectの話題がTechBull LT #3で出てきたこと
  • 個人開発しているアプリのバックエンドに使ってみたかったこと

が重なり、書くことにしました。

設定

記事書いている人のプロフィール

フロントエンドは全くと言っていいほど書けないですが、書く努力はしようと何度か試みたことのあるレベルの人です。本記事ではReact NativeとGoを用いてConnectをを使ってみたよっていう紹介(Not 解説)記事になります。また、試みたことがあるレベルの人間なのでもちろんReact Nativeすらおぼつかないレベルになります、ご容赦ください。

読者像について

対象となり得る読者

  • Connect使ったことないよー🙋‍♂️てお友達

逆に対象にならなさそうな読者

  • フロントエンド得意だよー😎て人
  • Connectをすでに使ったことがあるよー💪て人

表記について

コピペ目的ではなく実行結果を示したい場合はコマンドに$をつけています。
例えば、次の通りです。

コピペ目的
curl \
-H 'Content-Type: application/json' \
-d '{"shelf": "1"}' \
http://localhost:8080/bookstore.v1.BookstoreService/ListBooks
結果目的
$ curl \
-H 'Content-Type: application/json' \
-d '{"shelf": "1"}' \
http://localhost:8080/bookstore.v1.BookstoreService/ListBooks

{"books":[{"id":"1", "name":"Effective Go", "author":"Alan A. A. Donovan & Brian W. Kernighan"}, ..., {"id":"10", "name":"You Don't Know JS", "author":"Kyle Simpson"}]

本文

ConnectとConnectを利用する背景

Connectとは Buf Technologies,Incが開発しているHTTPとgRPC互換のAPIの両方を生成するツールです。

Connectを利用した場合でも、gRPCでもおなじみのProtocol Buffersを利用したスキーマ駆動の開発を行うことができます。
gRPCを用いた開発では、フロントエンド(ex: ブラウザ)からHTTPでgRPCを呼ぶにはHTTP->gRPCへ変換(Transcode)するためにEnvoyProxygrpc-gatewayを用いる、あるいは、サーバー側でgRPC Webに対応する必要がありました。

Connectを利用することで、前述したEnvoyProxyなどのProxyの利用やサーバー側でgRPC Web対応するコードを書くことなく、gRPC <-> gRPC Web間の通信ができるコードを生成することができます。

個人開発でもスキーマ駆動で開発をしたいけど、grpc-gatewayやEnvoyProxy使って変換するのは流石に大掛かりだな・・・と思っている人もこれで緩和されると期待しています。

サンプル実装するもの

今回はサーバー側のサンプルとしては、Googleさんが用意しているBookstoreListBooksを模倣し、フロントエンドはReact NativeのリストViewでBookstoreサービスから取得した本の一覧を表示するサンプルを作成します。

事前準備

環境を構築していきます。
以下の要件が満たされていれば、インストール方法・実行方法は自由に選んで大丈夫です。

  • Go: 1.25.4
  • Buf: 1.61.0
  • grpcurl: 1.9.3
  • Node: 24.11.1
  • React Native: 0.81.5

必要ツールの手順

Goのインストール:

brew install go

bufコマンドのインストール:

brew install bufbuild/buf/buf

サーバーの挙動テストをするためのgrpcurlコマンドのインストール:

brew install grpcurl

プロジェクトの準備

ディレクトリ構造を次のように分けました。

./
├── api             # GoのAPIサーバー用のディレクトリ
   └── go.mod
├── buf.gen.yaml    # bufコマンドを利用して生成するコードの設定ファイル
├── native          # ReactNative用のディレクトリ
└── proto           # Protocol Buffersファイル用のディレクトリ
    ├── bookstore
    └── buf.yaml    # bufのlint設定や破壊的変更検知設定ができるファイル

早速Protocol Buffersの定義のための準備を進めていきます。
まず、bufコマンドを用いてProtocol Buffersからコードを自動生成するには、buf.gen.yamlというファイルに生成するコードの設定を定義する必要があります。

プロジェクトルートディレクトリで、buf.gen.yamlを作成して次の設定を追加してください。

version: v2
plugins:
  - remote: buf.build/protocolbuffers/go:v1.36.10
    out: api/gen/go
    opt: paths=source_relative
  - remote: buf.build/bufbuild/connect-go:v1.10.0
    out: api/gen/go
    opt: paths=source_relative

ここでやっているのは、Protocol BuffersをGo用のコードにコンパイルするためには、protoc-gen-goが必要で、かつそのコンパイラを利用するためにはいろんな周辺ライブラリが必要になるのですが、bufがリモートプラグインというものを用意してくれており(remoteのフィールドに設定しているもの)、それを利用することで煩わしいインストールを飛ばしてコード生成することができます。
同様にconnect-goもリモートプラグインを利用しています。

outは生成したコードを配置するディレクトリを指定します。

次に、コードの生成元となるprotoファイルを作成します。
※今回はapi/gen/go配下に作成します。

利用するproto定義
bookstore.proto
syntax = "proto3";

package bookstore.v1;

option go_package = "github.com/icchy-san/bookstore-app/api/gen/go/bookstore/v1";

service BookstoreService {
    rpc ListBooks (ListBooksRequest) returns (ListBooksResponse);
}

message ListBooksRequest {
    string shelf = 1;
}

message ListBooksResponse {
    repeated Book books = 1;
}

message Book {
    string id = 1;
    string name = 2;
    string author = 3;
}

bufコマンドでlintの実行もできるので、興味があれば合わせてご確認ください(本記事では利用しなくても困らない)。

lintなどの任意の設定
buf config init

コマンドを実行すると、buf.yamlというファイルが生成されると思いますが、こちらのファイルにProtocol Buffersのファイルをコンパイルしてコード生成したファイルの配置場所や、bufコマンドによるLintの設定を記載することができます。
僕は次のように設定しました。(pluginsフィールド以外は自動生成されたものを利用しています。)

version: v2
lint:
  use:
    - STANDARD
breaking:
  use:
    - FILE

設定を終えて、protoディレクトリ配下で

buf lint

を実行すると、lintが実行されます。

実行結果(正常系)↓

$ buf lint

試しに、BookstoreServiceという名前からServiceを削除してlintを実行すると次のようなエラーになります。

実行結果(エラー)↓

$ buf lint
bookstore/v1/bookstore.proto:7:9:Service name "Bookstore" should be suffixed with "Service".

Goのスタブコード生成と挙動確認

さて、ここまで来たら、まずはGoのスタブコードを生成します。
プロジェクトルートディレクトリへ移動して次のコマンドを実行し、api/gen配下にbookstore.pb.goファイルが生成されていると思います。これがスタブコードで、Goのアプリケーションでimportして利用します。

buf generate proto

簡単に作成できてしまいます、、、!おそらく次のようなディレクトリ構造で出力されているかと思います。

api/gen配下のディレクトリ
api/gen
└── go
    └── bookstore
        └── v1
            ├── bookstore.pb.go
            └── v1connect
                └── bookstore.connect.go

ここまで来たらあとはGoのコードを実装するだけです。
この先のAPI実装については今回の本筋とズレるため、僕は決まった値を返す実装にとどめています。(=> DBとか使ったりせずレスポンスをハードコード)

実装したコードはこちら

https://github.com/icchy-san/bookstore-app

せっかくなので、サーバーを起動してTerminalからリクエストを送ってみましょう。
一般的なgRPCサーバーの場合は、grpcurlを利用する必要がありますが、Connectの場合は、curlでもリクエストが送れるとのことなので両方ためしてみます。

まずは、いつも通りのgrpcurlの場合:

grpcurl
$ grpcurl -protoset <(buf build -o -) -plaintext localhost:8080 bookstore.v1.BookstoreService/ListBooks

{
  "books": [
    {
      "id": "1",
      "name": "Effective Go",
      "author": "Alan A. A. Donovan \u0026 Brian W. Kernighan"
    },
    ...
     {
      "id": "10",
      "name": "You Don't Know JS",
      "author": "Kyle Simpson"
    }
  ]
}

次に、本命のcurlの場合:

grpcurl
$ curl \
-H 'Content-Type: application/json' \
-d '{"shelf": "1"}' \
http://localhost:8080/bookstore.v1.BookstoreService/ListBooks

{"books":[{"id":"1", "name":"Effective Go", "author":"Alan A. A. Donovan & Brian W. Kernighan"}, ..., {"id":"10", "name":"You Don't Know JS", "author":"Kyle Simpson"}]

確かにenvoy filtergrpc-gatewayを利用することなくcurlでデータが取得できていることがわかります。
ここまできたら次はReact Nativeのプロジェクトを作成して接続確認をしていきます。

React Nativeプロジェクトの初期化

さっそくReact Nativeのプロジェクトを初期化しましょう。
初期化については、分かる人は好きな方法でプロジェクトを作成してください。

初期化

自分はpnpmを利用しているため、pnpmでReact Natvie(Expo)のプロジェクトを初期化します。

pnpm create expo-app

これを実行すると、次のようにプロジェクト名を聞かれるので、好き名前を設定して、プロジェクトを初期化してください。名前聞かれた後にもいくつかステップがありますが、お好みの設定で大丈夫です。

$ pnpm create expo-app
What is your app named? my-app

自分は、nativeとしました。

コマンドの実行が完了するとnativeディレクトリが作成されていると思うので、プロジェクトのディレクトリへ移動してpnpm run startを実行してiOSやAndroidのシミュレーター、あるいは実機で実行して、アプリが起動して次図の画面が表示できたら初期化完了です。

ReactNativeのTopページの表示

さて、React NativeでConnectRPCを利用する準備をしていきます。
今回はConnectRPCを利用することを検証するのみなので、不要なページは削除し、なるべくシンプルにします。

Connect部分だけ見たい人は、削除パートはスキップでOKです。

不要なファイル群を削除

app/_layout.tsxを次のように変更を加えます。

app/_layout.tsx
  return (
    <ThemeProvider value={colorScheme === 'dark' ? DarkTheme : DefaultTheme}>
      <Stack>
-        <Stack.Screen name="(tabs)" options={{ headerShown: false }} />
-        <Stack.Screen name="modal" options={{ presentation: 'modal', title: 'Modal' }} />
+        <Stack.Screen name="index" options={{ headerShown: true, title: 'Home Page' }} />
      </Stack>
      <StatusBar style="auto" />
    </ThemeProvider>
  );

また、不要なファイル群も削除しましょう。次のファイルたちを削除しました。

deleted:    native/app/(tabs)/_layout.tsx
deleted:    native/app/(tabs)/explore.tsx
deleted:    native/app/(tabs)/index.tsx
deleted:    native/app/modal.tsx

Connectを利用してデータをAPIサーバーから取得して表示するためのViewファイルを作成します。
自分はファイルを削除したため、Viewファイル用のディレクトリ(app)構造としては次のようになっています。

native/app/
├── _layout.tsx
└── index.tsx

TypeScript用のstubコードを生成する場合は、buf.build/bufbuild/es:v2.10.1を利用するので、buf.gen.yamlに対して次のように変更を加えてbuf generate protoを実行します。

buf.gen.yaml
+- remote: buf.build/bufbuild/es:v2.10.1
+  include_imports: true
+  out: native/gen/
+  opt: target=ts

target=tsを設定しないと*.jsファイルが生成されます。そのため、TypeScriptを利用する場合は、target=tsを追加しましょう。

追記後、buf generate protoを実行すると、gen/bookstore/v1配下にbookstore_pb.tsファイルが生成されます。

React Nativeのアプリからgrpc-webを用いたGoのAPIと疎通実装

ファイルが生成されたことを確認できたら、React NativeのアプリからConnectRPCを用いて実装したAPIを呼ぶ部分の実装を行います。

詳しいコードはAPI同様にこちらのRepositoryを確認してください。

generate protoで生成されたファイルの中に、Bookの型やBookstoreServiceが定義されています。それぞれ、protoで定義したmessage BookService BookstoreServiceに該当します。
そのため、Bookはmessageのfieldを持ち、BookstoreServicelistBooksというサービスを持ちます。React NativeでGoのAPIサーバーへリクエストを送るために次のコードを記述します。好きなコンポーネントに次のコードを記述してください。

import { createConnectTransport } from "@connectrpc/connect-web";
import { BookstoreService } from "@/gen/bookstore/v1/bookstore_pb";

// ...

const client = createClient(BookstoreService, createConnectTransport({
    baseUrl: `http://${host}:${port}`,
}));

このように記述することで、BookstoreのGoのAPIに対してリクエストを送るためのgRPC Clientを生成することができます。gRPC Clientはその名の通り、gRPC経由でserviceを実行するためのClientになります。このClientがServiceを持っているので、listBooksを呼ぶことができます。

例えば次のように記述することで、listBooks serviceを呼ぶことができます。

const response = await client.listBooks({
    shelf: "1", // Dummyなのでこの値で何か変わるということはない。
});

ここまで書けたら、React NativeとGoのAPIサーバーを起動して疎通テストしてみます。

  • React Native
    • pnpm run server
  • Go Server
    • go run ./cmd/server

これで立ち上げることができます。
あとはChrome含むご自身が利用しているブラウザ経由で、React Nativeのアプリにアクセスしましょう。うまく動いていれば次の様な表示になると思います(あくまで僕と同じコードを書いていたら)。

トラブルシュート

CORSエラーが出る場合

スマホからlocalhost:8080ではAPI Serverに接続できないので、React NativeのアプリをChromeから立ち上げてみましょう。

・・・。もしかすると以下のようなエラー(CORSエラー)が出るかもしれません。

Access to fetch at 'http://localhost:8080/bookstore.v1.BookstoreService/ListBooks' from origin 'http://localhost:8081' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

その場合は、Go側で対応する必要があるので、一旦CORSエラーを回避するためのコードを追加しましょう。

middleware/cors.go
import (
	"net/http"

	connectcors "connectrpc.com/cors"
	"github.com/rs/cors"
)

func WithCORS(h http.Handler) http.Handler {
	middleware := cors.New(cors.Options{
        // *で全部許可なので、制御したい場合はここを変更すること。
		AllowedOrigins: []string{"*"},
		AllowedMethods: connectcors.AllowedMethods(),
		AllowedHeaders: connectcors.AllowedHeaders(),
		ExposedHeaders: connectcors.ExposedHeaders(),
	})
	return middleware.Handler(h)
}

今回はサンプルなので、AllowedOrigins: []string{"*"}という形で全部OKしてます。
このmiddlewareでGoのAPIで作成したhandlerをラップして上げることで解決できます。
次図のように本のリストが1つでも表示されていればgRPCサーバーからデータが取得できています。

access_successfully

これにて、bufを使ったReact Native + GoのAPIサンプル作成を通した入門記事は終わりになります。よりよいmiddlewareの実装や、React NativeのComponentの実装(ex: connect-queryを使ってキャッシング)はあると思いますが、あくまで入門編ということで、できる限り編集するファイルは減らしたり、シンプルにしたりしたつもりです。

不備などがあればこっそりと教えていただけると幸いです。

スマホの実機からAPIを呼ぶ方法

本筋と外れるため、深堀りはしないですが、
実機のアプリからPC上で動いているAPIサーバーへ接続する方法はあるので紹介します。

ズバリ、Private IPアドレスを使用することです。ルーターから利用しているPCに割り当てられているIPアドレスがあると思うので、そちらをReact Nativeのhost変数に設定することで、アクセス先がローカルホストではなく、特定の(この場合はAPIサーバーが起動している)PCになるため、APIを呼ぶことができるようになります。

Macを利用している場合は、ifconfigコマンドで調べる方法や、Wi-Fiボタンをoptionキーを押しながらクリックすることで表示できます。わかりやすい記事があったのでおいておきます。

さいごに

今回試しにbufのConnectを利用して開発をしてみましたが、リモートプラグインやlintがbuf側で用意されていたり、gRPC Web対応のサーバー実装も簡単にできたりと、開発体験がとてもいいことに気づきました。
もちろんシンプルなAPIや、フロントの実装しかしていないため、まだまだ深淵は覗けていないですが、さくっと試した感想としてはスキーマ駆動で開発できて、gRPC+gRPC Web with grpc-gatewayと比べると環境構築コストが低く、試しやすかったです。

実際に個人開発しているアプリの裏側もこの構成に変えてより深みに入っていこうと思います。

ありがとうございました。

参考

Discussion