ばとらの部屋
ConnectRPCを使ったスキーマ駆動開発をやってみた
作成日 2025年9月7日 / 最終更新日 2025年10月11日
8月から開始したスタートアップのインターン先で、バックエンド開発にprotobuf + ConnectRPCを採用していたため、初めてですがその知見をまとめようと思いました。
ConnectRPCは、Protocol Buffersを使用した現代的なRPCフレームワークです。Buf Technologies社によって開発され、高いパフォーマンスとプロトコルの優位性が特徴的な次世代のAPIフレームワークとして注目されています。
ConnectRPCはgRPCの進化形と位置づけられ、以下のような違いがあります。
ConnectはGo, JavaScript/TypeScrip(Node.js), Swift/Kotlinなどの言語でサポートされ、公式サイトに各言語での導入方法が記載されています。
ConnectRPCを使って、APIキーを用いた認証を行う簡単なサーバーを実装してみます。
サーバーサイドはNode.js(TypeScript)を採用します。ConnectRPCの公式サイトに簡単な導入方法が記載されていますのでそちらも参考にしてみてください。
Getting started | Connect
Connect-Node is a library for serving Connect, gRPC, and gRPC-Web compatible HTTP APIs using Node.js.
https://connectrpc.com/docs/node/getting-started

余談:Node.jsはv23.6.0からts-nodeやtsxなどのトランスパイラを介さずとも、直接TypeScriptを実行できるようになりました
mkdir api-auth-example
cd api-auth-example
npm init -y
npm install typescript
npx tsc --init
npm install @bufbuild/buf @bufbuild/protobuf @bufbuild/protoc-gen-es @connectrpc/connect./schema/app/v1ディレクトリを作成し、その中にapp.protoファイルを作成します。
syntax = "proto3";
package app.v1;
message PostRequest {
string name = 1;
}
message PostResponse {
string message = 1;
}
service PostService {
rpc Post(PostRequest) returns (PostResponse) {}
}./schemaディレクトリにbuf.yamlファイルを作成します。
version: v2
modules:
- path: .
lint:
use:
- STANDARD
breaking:
use:
- FILE./schemaディレクトリにbuf.gen.yamlファイルを作成します。
version: v2
plugins:
- remote: buf.build/connectrpc/es
out: gen
opt:
- target=ts./schemaディレクトリに移動し、以下のコマンドを実行します。
buf generate./schema/app/v1ディレクトリにapp_connect.tsとapp_pb.tsの2つのファイルが生成されます。
作業ディレクトリでserver.tsファイルを作成し、以下のコードを記述します。
import http from "http";
import { connectNodeAdapter } from "@connectrpc/connect-node";
import { Code, ConnectError, type ConnectRouter, type Interceptor } from "@connectrpc/connect";
import { PostService } from "./schema/gen/app/v1/app_pb.ts";
import dotenv from "dotenv";
dotenv.config();
const authInterceptor: Interceptor = (next) => async (req) => {
const expectedApiKey = process.env.API_KEY;
if (!expectedApiKey) {
throw new ConnectError('API key not configured on server', Code.Internal);
}
const authHeader = req.header.get('authorization');
if (!authHeader) {
throw new ConnectError(
'Authorization header is required',
Code.Unauthenticated,
);
}
// ヘッダーが "Bearer <API_KEY>" の形式であることを確認
if (authHeader.startsWith('Bearer ')) {
const token = authHeader.substring('Bearer '.length);
if (token !== expectedApiKey) {
throw new ConnectError('Invalid API key', Code.Unauthenticated);
}
} else {
throw new ConnectError(
'Invalid authorization scheme',
Code.Unauthenticated,
);
}
return await next(req);
};
const routes = (router: ConnectRouter) => {
router.service(PostService, {
post: async (req) => {
const name = req.name;
console.log("Received name:", name);
return { message: `Hello, ${name}!` };
},
});
}
const adapter = connectNodeAdapter({
routes,
interceptors: [authInterceptor],
});
const server = http.createServer(adapter);
server.listen(8080, () => {
console.log("Server is running on http://localhost:8080");
});ミドルウェアを実装する際はInterceptorを使用します。
ConnectRPCにおけるInterceptorは、RPCリクエストとレスポンスの処理フローを変更したり、追加の処理を挟み込んだりするための仕組みです。リクエストの前処理やレスポンスの後処理、エラーハンドリングなどを行うことができます。
const sampleInterceptor: Interceptor = (next) => async (req) => {
// リクエスト前の処理
const res = await next(req);
// レスポンス後の処理
return res;
};エラーハンドリング
throw new ConnectError('Invalid API key', Code.Unauthenticated);Interceptorはサーバーやクライアントの初期化時に適用します。
// サーバー側
const adapter = connectNodeAdapter({
routes,
interceptors: [authInterceptor, loggingInterceptor], // 例えば複数のInterceptorをチェーンできる
});
// クライアント側
const client = createPromiseClient(
PostService,
createConnectTransport({
baseUrl: "http://localhost:8080",
interceptors: [authInterceptor],
})
);作業ディレクトリに.envファイルを作成し、以下の内容を記述します。
API_KEY=your_api_key_here # ここに任意のAPIキーを設定APIキーは他人に知られないようにランダムな文字列を設定してください。
サーバーを起動します。
node server.tscurlコマンドを使って、APIキーを用いたリクエストを送信します。
curl -X POST http://localhost:8080/app.v1.PostService/Post \
-H "Content-Type: application/json" \
-H "Authorization: Bearer your_api_key_here" \
-d '{"name": "batora9"}'
{"message":"Hello, batora9!"}というレスポンスが返ってきたので成功です。