every Tech Blog

株式会社エブリーのTech Blogです。

Vercel Labs の「emulate」を試してみた

はじめに

こんにちは、リテールハブ開発部の杉森です。

Vercel Labs が開発しているローカル API エミュレータ「emulate」が面白そうだったので、実際に触りながら AWS SDK (S3) との互換性、GitHub / Google の OAuth フロー、本番 API への切り替えまでを試してみました。

emulate とは

emulate は Vercel Labs が開発しているオープンソースのローカル API エミュレータです。GitHub、Google、Slack、Stripe、AWS など 12 のサービスをローカルで再現でき、単なるモック(固定レスポンスを返すだけ)ではなく、ステートフルなデータストアと OAuth フローを備えています。

npx emulate の1コマンドで 12 サービスがポート 4000〜4010 で起動します。設定ファイルは不要で、デフォルトのユーザーとトークンが自動で作成されます。

emulate v0.4.1

vercel       http://localhost:4000
github       http://localhost:4001
google       http://localhost:4002
slack        http://localhost:4003
apple        http://localhost:4004
microsoft    http://localhost:4005
okta         http://localhost:4006
aws          http://localhost:4007
resend       http://localhost:4008
stripe       http://localhost:4009
mongoatlas   http://localhost:4010

Tokens
test_token_admin -> admin

起動直後から Authorization: Bearer test_token_admin で全サービスの API を呼び出せます。

# ユーザー情報を取得
curl http://localhost:4001/user -H "Authorization: Bearer test_token_admin"

# リポジトリを作成
curl -X POST http://localhost:4001/user/repos \
  -H "Authorization: Bearer test_token_admin" \
  -H "Content-Type: application/json" \
  -d '{"name":"hello-world"}'

レスポンスは本物の GitHub API と同じ JSON 構造です。ステートフルなので、上で作成したリポジトリに対して Issue や PR を追加するといった操作もできます。

実際に試してみた

emulate v0.4.1 / Node.js v22 の環境で、以下の3つの観点から検証しました。

  1. AWS SDK (S3) との互換性: SDK 経由でそのまま使えるか
  2. GitHub / Google OAuth フローの実装: OAuth を含むアプリをローカルで開発できるか
  3. 本番 API への切り替え: 2 で作ったアプリのコードを変えずに本番で動かせるか

AWS SDK (S3) との互換性を検証する

emulate の AWS エミュレータが、AWS SDK v3 からそのまま使えるかを検証しました。検証には @aws-sdk/client-s3 3.1028.0 を使用しています。

emulate の認証の仕組み

emulate ではすべてのサービスが Bearer トークン認証に統一されています。実際のサービスではそれぞれ認証方式が異なりますが、emulate 上ではどれも同じ Authorization: Bearer でアクセスできるようになっています。

また、登録されていないトークンでリクエストしても、フォールバック機構によりデフォルトユーザー(admin)として認証が通ります。テストコードでトークンの値を気にせず書けるのは便利でした。

この仕組みが AWS SDK との互換性に直接関わってきます。

困った点と対応内容

AWS SDK v3 から emulate の S3 エミュレータを使うには、以下の2つの対応が必要でした。

1. endpoint のパスが合わない

emulate のルートは /s3/:bucket/:key 形式ですが、AWS SDK は /bucket/key にリクエストを送るため、パスが一致しません。endpoint を localhost:4007/s3 にすることでパスを合わせます。

2. 末尾スラッシュでルートが一致しない

AWS SDK は PUT /s3/bucket-name/ のように末尾スラッシュ付きでリクエストを送りますが、emulate のルート定義にスラッシュがないためマッチしません。SDK ミドルウェアで末尾スラッシュを除去することで対応しました。

対応後の動作結果

上記2つの対応を入れることで、CreateBucket、PutObject、GetObject、ListObjectsV2、ListBuckets、DeleteObject といった主要な S3 操作はすべて動作しました。

なお、AWS SDK は SigV4 署名を送りますが、emulate は SigV4 を解釈しません。前述のフォールバック認証によりデフォルトユーザーとして通るため、credentials の値は { accessKeyId: "dummy", secretAccessKey: "dummy" } で動きます。

ただし、Presigned URL、S3 イベント通知、SQS との連携などは未対応です。AWS SDK のより広い互換性が必要な場合は別のライブラリ等を検討した方が良さそうです。emulate の AWS エミュレータは「REST API の形状をテストする」用途向けという印象です。

補足: 本記事で触れたルートのパス不一致や Presigned URL 未対応については、修正の PR がすでに存在しております。マージされれば上記の回避策は不要になりそうです。

OAuth フローを組み込む

emulate を使ってみて特に便利だと思ったのは OAuth フローのエミュレーションです。GitHub OAuth でサインインして PR 一覧を表示するアプリを作って検証しました。

シード設定で初期データを定義する

emulate は YAML で初期データ(シード)を定義できます。起動時にユーザー、リポジトリ、OAuth App が自動で作成されます。

github:
  users:
    - login: admin
      name: Admin User
      email: admin@example.com
  repos:
    - owner: admin
      name: test-repo
      auto_init: true
  oauth_apps:
    - client_id: emu_github_client_id
      client_secret: emu_github_client_secret
      name: PR Viewer App
      redirect_uris:
        - http://localhost:3000/auth/callback

以下は、今回作ったアプリの OAuth フローです。

emulate にアクセスすると、以下のような認可画面が表示されます。

シードで定義したユーザーをクリックするだけで認可が完了します。トークン交換や API コールは本物の GitHub API と同じエンドポイントで動作するため、アプリ側のコードは本番と同じ実装がそのまま使えます。

なお、シード設定は宣言的なデータ定義のみに対応しており、PR のようなリソースはシードで作成できません。API 経由で投入する必要があります。

Node.js から直接起動して開発環境を自動化する

emulate は CLI( npx emulate )だけでなく、Node.js のコードから直接起動する API( createEmulator )も提供しています。これを使って、emulate の起動、テストデータの投入、Web サーバーの起動を1コマンドにまとめました。

import { createEmulator } from "emulate";

const github = await createEmulator({ service: "github", port: 4001, seed: config });
const google = await createEmulator({ service: "google", port: 4002, seed: config });

npm run dev だけで全部起動する体験は快適でした。

本番 API への切り替えを検証する

server.js はすべてのエンドポイント URL を process.env から読み取る設計にしました。emulate と本番の切り替えは .env.local と .env の読み分けだけで行えます。

# .env.local(emulate 用)
GITHUB_URL=http://localhost:4001
GITHUB_API_URL=http://localhost:4001
GITHUB_CLIENT_ID=emu_github_client_id
GITHUB_CLIENT_SECRET=emu_github_client_secret
GITHUB_OWNER=admin
GITHUB_REPO=test-repo

# .env(本番用)
GITHUB_URL=https://github.com
GITHUB_API_URL=https://api.github.com
GITHUB_CLIENT_ID=<実際の Client ID>
GITHUB_CLIENT_SECRET=<実際の Client Secret>
GITHUB_OWNER=<実際のオーナー名>
GITHUB_REPO=<実際のリポジトリ名>
{
  "scripts": {
    "dev": "node --env-file=.env.local dev.js",
    "start:prod": "node --env-file=.env server.js"
  }
}

本番の GitHub OAuth App を作成し .env に設定して npm run start:prod で起動したところ、コード変更なしで本物の GitHub 認可画面が表示され、実際の PR 一覧が取得できました。

観点 emulate 本番 GitHub
認可画面 emulate のユーザー選択画面 GitHub の実際の認可画面
認可の操作 ユーザーをクリック 「Authorize」ボタン
データ テストデータ リポジトリの実際の PR
API キー 不要 実際の Client ID / Secret

Google OAuth + Gmail API も同じパターンで追加しました。emulate 用の環境変数は以下の通りです。

# .env.local(emulate 用)
GOOGLE_URL=http://localhost:4002
GOOGLE_TOKEN_URL=http://localhost:4002/oauth2/token
GOOGLE_API_URL=http://localhost:4002
GOOGLE_CLIENT_ID=emu_google_client_id
GOOGLE_CLIENT_SECRET=emu_google_client_secret

# .env(本番用)
GOOGLE_URL=https://accounts.google.com
GOOGLE_TOKEN_URL=https://oauth2.googleapis.com
GOOGLE_API_URL=https://www.googleapis.com
GOOGLE_CLIENT_ID=<実際の Client ID>
GOOGLE_CLIENT_SECRET=<実際の Client Secret>

まとめ

3つの検証観点ごとに結論を整理します。

  1. AWS SDK (S3) との互換性: endpoint のプレフィックス追加と末尾スラッシュ除去の2つの回避策を入れれば、主要な S3 操作は動作する。ただし Presigned URL 等は未対応で、より広い互換性が必要なら 別のライブラリを検討した方が良い。

  2. GitHub / Google OAuth フローの実装: シード設定と認可画面の自動生成により、OAuth App の登録やテストユーザーの作成なしで OAuth フローを含むアプリの開発を始められる。OAuth フローをローカルで手軽にテストできるのは便利だった。

  3. 本番 API への切り替え: エンドポイント URL を環境変数に切り出しておけば、コード変更なしで本番に切り替えられる。

興味がある方はぜひ試してみてください。

参考リンク

emulate GitHub リポジトリ
emulate 公式ドキュメント