mcpField
V
vtex-io-http-routes

vtex-io-http-routes

github.com/vtex開発471.2k 回インストール2 週間前に更新
リポジトリ

vtex-io-http-routes について

VTEX IO サービス開発者向けに、HTTP ルートの宣言、ハンドラーの境界、中間ウェアの構成を案内するスキルです。明示的な HTTP 契約を重視し、時間のかかる処理を非同期フローに分けます。

主な機能

  1. 01service.json のルート宣言とハンドラー接続
  2. 02境界でのリクエスト検証とレスポンス整形
  3. 03中間ウェアによる検証・認証の共通化
  4. 04HTTP リクエストと非同期バッチ処理の分離

活用シーン

  1. 01コールバックや Webhook エンドポイントの構築
  2. 02パートナーやバックオフィス向け連携 API の公開
  3. 03ルートハンドラーと中間ウェアチェーンの整理
  4. 04ルートのパラメーター、応答、ステータスコードの確認

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/vtex/skills --skill vtex-io-http-routes

インストール後、Claude Code で/vtex-io-http-routesを入力して読み込まれたか確認します。

詳細

HTTP ルートとハンドラーの境界

このスキルを使用する場面

VTEX IO サービスで、service.json のルートを通じて明示的な HTTP エンドポイントを公開し、対応するハンドラーを node/ 以下に実装する場合に、このスキルを使用します。

  • コールバックまたは Webhook エンドポイントの構築
  • パートナーやバックオフィスのワークフロー向けの連携エンドポイントの公開
  • ルートハンドラーとミドルウェアチェーンの構成
  • パラメーター、クエリ文字列、ヘッダー、リクエストボディの検証
  • レスポンスの形式とステータスコードの動作の標準化

以下には、このスキルを使用しないでください。

  • サービスランタイムのサイジングやチューニング
  • manifest.json でのアプリポリシーの決定
  • GraphQL API の設計
  • 非同期イベントやワーカーフローのモデル化

判断ルール

  • 連携で明示的な URL 契約、Webhook、またはコールバック形式のリクエストとレスポンスの動作が必要な場合は、HTTP ルートを使用します。
  • VTEX IO では、service.json でルート ID、パス、public などの公開設定を宣言し、Node のエントリーポイントでそれらのルート ID を node/routes からエクスポートされたハンドラーに結び付けます。ミドルウェアはコード内で組み合わせます。service.json に直接宣言するものではありません。
  • ルートハンドラーは小さく、明確に保ちます。ルートコードでは入力を検証し、ドメインサービスまたは連携サービスを呼び出して、レスポンスを組み立てます。
  • 検証、リクエストの正規化、共通の認証チェックなどの横断的な関心事は、各ハンドラーで重複させず、ミドルウェアに配置します。
  • ルートのパラメーター、クエリに関する想定、ボディの形式は、ハンドラーの境界にできるだけ近い場所で定義します。
  • 類似するルート群では、一貫したステータスコードとレスポンス構造を使用します。
  • Webhook またはコールバックエンドポイントでは、呼び出し元が文書化しているステータスコードとエラーボディの要件に従い、曖昧な再試行を避けるため、レスポンスは小さく予測可能なものにします。
  • 重要なルートでは構造化ログまたはメトリクスを出力し、ハンドラーの契約を変更することなく、障害、レイテンシー、連携の健全性を診断できるようにします。
  • routes/orders.ts や routes/catalog.ts のように、境界づけられたドメインごとに明示的なルートファイルをまとめることを推奨します。
  • 公開ルートは、明示的な外部契約として扱います。検証、認証要件、レスポンスの安全性を確認せずに、ルートを公開用途に拡張してはいけません。

厳守事項

制約:ルートハンドラーでは HTTP 契約を明示する必要があります

各ルートハンドラーでは、ハンドラーの境界でリクエストとレスポンスの契約を理解できるようにする必要があります。必須パラメーター、ボディフィールド、ステータスコードの判断を、無関係なサービスの深い階層に隠してはいけません。

この制約が重要な理由

HTTP 連携は予測可能な契約に依存します。検証やレスポンスの組み立てが暗黙的、またはあちこちに分散していると、パートナーとの連携は壊れやすくなり、エラーの診断も難しくなります。

検出方法

必須パラメーター、クエリ値、ヘッダー、またはリクエストボディの形式を検証せずに、ハンドラーが直ちに処理を委譲している場合は、作業を中断し、先に契約を明示してください。

適切な例

export async function getOrder(ctx: Context, next: () => Promise<void>) {
  const { id } = ctx.vtex.route.params

  if (!id) {
    ctx.status = 400
    ctx.body = { message: 'Missing route param: id' }
    return
  }

  const order = await ctx.clients.partnerApi.getOrder(id)
  ctx.status = 200
  ctx.body = order
  await next()
}

不適切な例

export async function getOrder(ctx: Context) {
  ctx.body = await handleOrder(ctx)
}
制約:共通のルート関心事は、各ハンドラーで繰り返すのではなく、ミドルウェアに配置する必要があります

検証、リクエストの正規化、共通の認証チェックなど、繰り返し発生する関心事は、ミドルウェアとして実装し、ルートチェーンを通じて組み合わせるべきです。

この制約が重要な理由

同じチェックを多くのハンドラーに重複して実装すると、差異が生じ、ルートの動作に一貫性がなくなります。ミドルウェアを使うことで、HTTP の公開面をレビューしやすく、発展させやすくできます。

検出方法

複数のハンドラーで、同じボディ検証、ヘッダーチェック、またはコンテキストの準備が繰り返されている場合は、作業を中断し、重複を増やす前にミドルウェアとして切り出してください。

適切な例

export async function validateSignature(ctx: Context, next: () => Promise<void>) {
  const signature = ctx.request.header['x-signature']

  if (!signature) {
    ctx.status = 401
    ctx.body = { message: 'Missing signature' }
    return
  }

  await next()
}

不適切な例

export async function routeA(ctx: Context) {
  if (!ctx.request.header['x-signature']) {
    ctx.status = 401
    return
  }
}

export async function routeB(ctx: Context) {
  if (!ctx.request.header['x-signature']) {
    ctx.status = 401
    return
  }
}
制約:HTTP ルートで、イベントやワーカーに任せるべき非同期処理やバッチ処理を行わない

ルートでは、リクエストとレスポンスのレイテンシを一定の範囲内に保つ必要があります。ルートでコストの高い処理、再試行が頻繁に発生する処理、またはバッチ処理を実行する場合は、その処理を非同期フローに移し、ルートはトリガーまたは確認応答の境界としてシンプルに保ってください。

重要な理由

実行時間の長い HTTP ハンドラーは、連携の動作を損ない、タイムアウトのリスクや運用の不安定さを招きます。VTEX IO サービスでは、即時に応答するルートの契約とバックグラウンド処理を分離する必要があります。

検出

ルートで大規模なループ、バッチインポート、負荷の高い再試行、または応答前に完了する必要のない処理を行う場合は、いったん停止し、非同期処理を中心としたフローに再設計してください。

正しい例

export async function triggerImport(ctx: Context) {
  await ctx.clients.importApi.enqueueImport(ctx.request.body)
  ctx.status = 202
  ctx.body = { accepted: true }
}

誤った例

export async function triggerImport(ctx: Context) {
  for (const item of ctx.request.body.items) {
    await ctx.clients.importApi.importItem(item)
  }

  ctx.status = 200
}

推奨パターン

推奨するファイル構成:

node/
├── routes/
│   ├── index.ts
│   ├── orders.ts
│   └── webhooks.ts
└── middlewares/
    ├── validateBody.ts
    └── validateSignature.ts

VTEX IO サービスでのルートの接続:

VTEX IO では、service.json でルート ID とパスを宣言し、Node のエントリーポイントで new Service(...) に routes オブジェクトを登録します。node/routes/index.ts では、各ルート ID を最終的なハンドラーにマッピングします。ミドルウェアはコード内で合成し、service.json で直接宣言することはありません。

{
  "routes": {
    "orders-get": {
      "path": "/_v/orders/:id",
      "public": false
    },
    "reviews-create": {
      "path": "/_v/reviews",
      "public": false
    }
  }
}
// node/index.ts
import type { ClientsConfig, RecorderState, ServiceContext } from '@vtex/api'
import { Service } from '@vtex/api'
import { Clients } from './clients'
import routes from './routes'

const clients: ClientsConfig<Clients> = {
  implementation: Clients,
  options: {
    default: {
      retries: 2,
      timeout: 800,
    },
  },
}

declare global {
  type Context = ServiceContext<Clients, RecorderState>
}

export default new Service<Clients, RecorderState>({
  clients,
  routes,
})

最小限のルートパターン:

// node/routes/index.ts
import type { RouteHandler } from '@vtex/api'
import { createReview } from './reviews'
import { getOrder } from './orders'

const routes: Record<string, RouteHandler> = {
  'orders-get': getOrder,
  'reviews-create': createReview,
}

export default routes
// node/routes/orders.ts
import { compose } from 'koa-compose'
import { validateSignature } from '../middlewares/validateSignature'

async function rawGetOrder(ctx: Context, next: () => Promise<void>) {
  const { id } = ctx.vtex.route.params

  if (!id) {
    ctx.status = 400
    ctx.body = { message: 'Missing route param: id' }
    return
  }

  const order = await ctx.clients.partnerApi.getOrder(id)
  ctx.status = 200
  ctx.body = order

  await next()
}

export const getOrder = compose([validateSignature, rawGetOrder])
export async function createReview(ctx: Context, next: () => Promise<void>) {
  const body = ctx.request.body

  if (!body?.productId) {
    ctx.status = 400
    ctx.body = { message: 'Missing productId' }
    return
  }

  const review = await ctx.clients.reviewApi.createReview(body)
  ctx.status = 201
  ctx.body = review
  await next()
}

ドメインロジックはサービスやインテグレーションに配置し、ルートハンドラーはバリデーション、ステータスコード、ヘッダー、レスポンス形式などの HTTP に関する処理を担当するようにしてください。

よくある失敗

  • ルートで期待される内容を明示せず、無関係なサービスにリクエストのバリデーションを隠してしまう。
  • ミドルウェアを使わず、同じ認証や正規化のロジックを複数のハンドラーで繰り返す。
  • HTTP ハンドラーで、実行時間の長い非同期処理やバッチ処理を行う。
  • 類似するエンドポイントで、ステータスコードやレスポンス形式に一貫性がない。
  • 信頼境界を見直さずに、ルートを外部に公開する。

レビューチェックリスト

  • この契約の公開方法として、HTTP は適切ですか?
  • 必須のパラメーター、ヘッダー、クエリ値、本文フィールドは、ルート境界でバリデーションされていますか?
  • 繰り返し使用する処理はミドルウェアにまとめられていますか?
  • ハンドラーは HTTP に関する処理に集中し、簡潔に保たれていますか?
  • 処理の一部を非同期イベントやワーカーに移すべきですか?

関連スキル

  • vtex-io-events-and-workers - コストの高い処理や再試行が頻繁に発生する処理を HTTP ハンドラーから非同期フローに移す場合に使用します
  • vtex-io-auth-and-policies - HTTP ルートを保護するポリシーやアクセスルールを判断する場合に使用します

参考資料

コメント