👋

[超モダン] Connectを使用したGo/DartのELIZAアプリ ~Go編~

2025/02/19に公開

こんな人におすすめ

  • スキーマ駆動開発に興味がある人
  • RESTとgRPCの両方に対応できる柔軟なAPIを作りたい人
  • 型安全なバックエンド・フロントエンドの開発を行いたい人
  • Bufに興味がある人
  • (Dockerを使ったMySQLのマイグレーションや、環境構築を行いたい人)

サンプルプロジェクトは、以下です!
https://github.com/rensawamo/dart-go-grpc-connect

Connect とは

Connectは、gRPCの機能を簡素化し、ブラウザと互換性のあるHTTP APIを構築するためのライブラリです。Connectを使うことで、以下のメリットがあります。

  • スキーマ駆動の開発
    Protobufを利用してAPI定義を作成し、バックエンドとフロントエンドで共通のコードを自動生成

  • シンプルな構築
    gRPCに比べて、設定やプロキシの導入が不要

  • HTTP/1.1 でも動作
    curlコマンドで簡単にリクエストをテスト可能

  • 様々なストリーミング対応
    Connect プロトコル、gRPC、gRPC WEB の 3 つのプロトコルに対応しています。
    従来の gRPC との互換性があるため HTTP/2 を用いた双方向ストリーミングも可能です。

https://connectrpc.com/

go-grpcからconnect-goに移行した話

最近、gRPCからConnectへ移行したという記事を会社のZennに投稿させて頂いたので、よければ一読ください。

https://zenn.dev/baleenstudio/articles/133bf2bccf515a

なぜ書こうと思ったのか

私のプロジェクトでは、主にBackend/BFFは、Goで、Frontendには、Dartを用いています。
しかし、ConnectはGoには対応していたのですが、Dartになかなか対応せず、対応を待っている状況でした。

しかし、最近Dartのドキュメントが更新されて、ConnectがDartでも使えるようになりました。

https://github.com/connectrpc/connect-dart
https://pub.dev/packages/connectrpc

また、公式ドキュメントも更新されて、Elizaアプリがサンプルとして提供されていたので、
JWT認証やインターセプターを実装した拡張アプリを作成し、記事にすることで、どなたかの役に立てればいいなと思い開発することにしました。

https://connectrpc.com/docs/dart/getting-started

Elizaとは

Elizaは、1960年代にMITのジョセフ・ワイゼンバウムによって開発された、初期の自然言語処理プログラムです。人間と対話するチャットボットとして設計され、特に心理療法士を模した「DOCTOR」スクリプトが有名です。Elizaはユーザーの入力に対してキーワードを基に応答し、簡単な対話を実現します。

本プロジェクトでは、このElizaの概念を取り入れ、Connect-Goを活用してモダンなバックエンドを構築し、マイグレーションで登録したシンプルな文をランダムで返すアプリとしました。

詳細はこちら: Wikipedia - ELIZA

サンプルプロジェクトの説明

スキーマファイルは、以下で定義されています。
https://github.com/rensawamo/dart-go-grpc-connect/tree/main/proto

①loginでアクセストークンを得る
②①で受けとったアクセストークンをHeaderに含めて、Elizaにリクエストを送る。
レスポンスとして、マイグレーションした文をランダムで受け取るといったシンプルなアプリです。

backend と frontendで共通のインターフェイスを提供

project-root/
│── backend/       # バックエンド(Go)のソースコード
│   ├── gen   
│── frontend/      # フロントエンド(Dart)のソースコード
│   ├── lib/gen   
│── proto/         # Protocol Buffersの定義ファイル

このプロジェクトでは、protoディレクトリに、Protocol Buffers の定義ファイルを配置し
フロントエンドとバックエンドの、両方で共通のプロトコルを使用できるようになっています。

具体的には、proto/ に .proto ファイルを定義し、それを Bufなどのコードジェネレーターを使って、フロントエンド(TypeScript, Dart など)と、バックエンド(Go, Python, Node.js など)で、それぞれ利用できるクライアントコードやサーバースタブを自動生成します。

これにより、フロントエンドとバックエンド間で一貫した API 仕様を維持することができます。

この構造により、フロントエンドとバックエンドの開発チームが、同じプロトコルを基に開発を進められ
API の整合性を保ちつつ型安全な通信を実現できます。

例えば、プロジェクトルートのMakeコマンドから、一斉にFrontendとBackendでのコードジェネレーションが可能です!

$ make gen
cd backend && make gen && cd ../frontend && make gen
....

アーキテクチャについて

アプリケーションは以下の層で構成されています

  • Mux
    クライアントリクエストの適切なhandlerへのルーティング

  • Handler
    リクエストの受付とUsecaseの呼び出し

  • Usecase
    ビジネスロジックの実装

  • Repository
    データベース操作の抽象化

  • Domain (Model/Entity)
    基本データ構造の定義

  • Dependency Injection (DI)
    依存関係の管理を行い、各レイヤーの疎結合化を実現

ディレクトリ構造は以下です。

backend/
├── api/
│   ├── handler/       # Muxに登録するHandler実装
│   ├── interceptor/   # JWT検証など
├── cmd/               # エントリーポイント
├── database/          # データベース関連
├── di/                # 依存性注入(DI)
├── domain/            # ドメインモデル
├── gen/               # コード生成(protobuf/SQLBoiler)
├── internal/          # 内部パッケージ
├── repository/        # リポジトリ実装
├── usecase/           # ユースケース実装

プロジェクトセットアップ

環境変数には、以下を用います。
https://github.com/direnv/direnv

プロジェクト用の暗号鍵を作成

$ ssh-keygen -t rsa -b 2048 -f $PRIVATE_KEY_PATH -N ""
$ ssh-keygen -p -m PEM -f $PRIVATE_KEY_PATH

backend/.envrcの環境変数を変更してください。

PRIVATE_KEY_PATH=YOURS

backendに移動してdirenvを読み込む

$ cd backend && direnv allow

DockerのMySqlを立ち上げる

$ make up
[+] Running 1/1
 ✔ Container database-db-1  Recreated   

マイグレーションアップ

※ 同じくディレクトリ環境変数を読み込み
ユーザーと、elizaが返す文章を登録しておきます。

$ make migrate-up
sql-migrate up --config dbconfig.yml
Applied 1 migrations

テーブルは、以下のような感じです!

elizaの返す文章を登録

id sentence
BLOB Hi
BLOB Hola
BLOB アンニョン
BLOB Bonjour

テストユーザの登録

id email password created_at updated_at
BLOB test@google.com example 2025-02-12 06:46:43 2025-02-12 06:46:43

本サンプルアプリでは、テストユーザをマイグレーションで事前登録し、フロントエンド側で簡単にログインできる仕組みを導入しています。

新規ユーザの作成は、フロントエンド側からの操作がやや面倒であるため、バックエンドのマイグレーション時にテストユーザを登録し、ログイン時にアクセストークンを取得する方式 を採用しました。

フロントエンドでは、画面が描画されたタイミングで TextController にテストユーザの情報(メールアドレス・パスワード)が自動入力されるように設定。これにより、ユーザーはワンクリックで簡単にログインできるようになっています。

この仕組みにより、認証の流れをスムーズに体験しつつ、最小限の手間でサンプルアプリの動作確認が可能となっています。

サーバーをたちげる

$ make run
go run cmd/main.go
Connected to database
Starting server at localhost:8080

上記のように、サーバーが立ち上がったら準備完了です。

API 操作をCurlコマンドで行ってみよう

ConnectはCurlコマンドにも対応しています。
では、実際にリクエストを送ってみましょう。

リクエストフロー

パブリックAPI

login認証で採用してます。

認証つきAPI

Eliza APIで採用してます。

こちらは、以下のようにInterceptorを登録することによって、
APIリクエストで、JWTの認証を確認しています。

func NewAuthInterceptor(issuer string, keyPath string) connect.UnaryInterceptorFunc {
	interceptor := func(next connect.UnaryFunc) connect.UnaryFunc {
		return connect.UnaryFunc(func(ctx context.Context, req connect.AnyRequest) (connect.AnyResponse, error) {

			authHeader := req.Header().Get("Authorization")
			if authHeader == "" {
				return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("missing authorization header"))
			}

			token := strings.TrimSpace(strings.TrimPrefix(authHeader, "Bearer"))
			if token == "" {
				return nil, connect.NewError(connect.CodeUnauthenticated, errors.New("empty token after trimming"))
			}

              ....
			return next(ctx, req)
		})
	}
	return connect.UnaryInterceptorFunc(interceptor)
}
// インターセプタの設定
authInterceptor := connect.WithInterceptors(interceptor.NewAuthInterceptor(issuer, keyPath))

mux.Handle(authv1connect.NewAuthServiceHandler(loginHandler))
mux.Handle(elizav1connect.NewElizaServiceHandler(elizaHandler, authInterceptor)) → 登録

login API

マイグレーションで登録したユーザでログインすると、アクセストークンが返却されます。

リクエスト

$ curl --location --request POST 'http://localhost:8080/auth.v1.AuthService/Login' \
--header "Content-Type: application/json" \
--data '{"email": "test@google.com", "password": "example"}'

または、プロジェクトのルートのMakefileから

$ make login

レスポンス

{
  "token": "access_token"
}

say API

上記で得たアクセストークンを用いて、ELIZAに話しかけます。

### リクエスト
$ curl --location --request POST 'http://localhost:8080/eliza.v1.ElizaService/Say' \
--header "Authorization: Bearer your_access_token" \
--header "Content-Type: application/json" \
--data '{"sentence": "Hi"}'

または、

$ make say

そうすると、マイグレーションで登録した文がランダムで返されます。

レスポンス

{
  "sentence": "Hi"
}

createSectence API

万が一、バリエーションが少なくて物足りないって時のために、
ELIZAが返却する文章を増やせるようにしました⭐️

リクエスト

$ curl --location --request POST 'http://localhost:8080/eliza.v1.ElizaService/CreateSentence' \
--header "Authorization: Bearer access_token" \
--header "Content-Type: application/json" \
--data '{"input": "add sentence"}'

または、

$ make create_sentence input="add sentence"

レスポンス

{
  "sentence": "add sentence"
}

最後に

スキーマファイルを最初に定義するだけで、モックを使った開発が可能になるため、開発効率がグンと伸びますよね!!
普段の開発で、この恩恵をとても感じております。

Frontendの記事は以下で書いてますので、よかったら覗いてみてください。

また、ユーザー登録や、リフレッシュトークンの処理を追加したりしてみると、勉強になるかと思います!!

お役に立てれば幸いです。

参考

以下のコードを大変参考にさせていただきました。
ありがとうございました。

株式会社BALEEN STUDIO

Discussion