mcpField
V
vtex-io-service-apps

vtex-io-service-apps

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

vtex-io-service-apps について

VTEX IOアプリにバックエンドロジックを実装する開発者向けのスキルです。サービスのエントリーポイント、ルート、ミドルウェア、イベントハンドラー、登録済みAPIクライアントを説明し、VTEX基盤に沿った実装方法を示します。

主な機能

  1. 01node/index.tsによるサービス構成
  2. 02ルートとミドルウェアチェーンの設定
  3. 03APIクライアントの作成と登録
  4. 04ctx.clients経由のクライアント利用
  5. 05リゾルバーとイベントの処理
  6. 06HTTP呼び出しとルート構成の確認

活用シーン

  1. 01VTEX IOアプリのREST APIルート構築
  2. 02VTEXまたは外部APIへの接続
  3. 03GraphQLリゾルバーやイベントハンドラーの追加
  4. 04クライアント登録とHTTP利用の確認

インストール

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

$npx skills add https://github.com/vtex/skills --skill vtex-io-service-apps

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

詳細

バックエンドサービスアプリと API クライアント

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

REST API ルート、GraphQL リゾルバー、イベントハンドラー、スケジュールされたタスク、VTEX Commerce API や外部サービスとの連携など、バックエンドロジックを必要とする VTEX IO アプリを開発するときに、このスキルを使用します。

  • 型付きコンテキスト、クライアント、状態を備えた Service エントリーポイント(node/index.ts)を構築する
  • JanusClient または ExternalClient を拡張するカスタムクライアントを作成して登録する
  • ctx.clients を使用して、キャッシュ、再試行、メトリクスが組み込まれたクライアントにアクセスする
  • service.json でルートとミドルウェアチェーンを設定する

次の用途にはこのスキルを使用しないでください。

  • Manifest と builder の設定(代わりに vtex-io-app-structure を使用してください)
  • GraphQL スキーマ定義(代わりに vtex-io-graphql-api を使用してください)
  • React コンポーネントの開発(代わりに vtex-io-react-apps を使用してください)

判断ルール

  • Service クラス(node/index.ts)は、すべての VTEX IO バックエンドアプリのエントリーポイントです。クライアント、ルート(ミドルウェアチェーンを含む)、GraphQL リゾルバー、イベントハンドラーを受け取ります。
  • すべてのミドルウェア、リゾルバー、イベントハンドラーは、次の情報を含む ctx を受け取ります。ctx.clients(登録済みクライアント)、ctx.state(リクエストごとに変更可能な状態)、ctx.vtex(認証トークン、アカウント情報)、ctx.body(リクエスト/レスポンスボディ)。
  • VTEX の内部 API(ベース URL:https://{account}.vtexcommercestable.com.br)には JanusClient を使用します。
  • VTEX 以外の API(指定した任意の URL)には ExternalClient を使用します。
  • 他の VTEX IO アプリが公開するルートには AppClient を使用します。
  • Master Data v2 の CRUD 操作には MasterDataClient を使用します。
  • IOClients を拡張してカスタムクライアントを登録します。各クライアントは、最初にアクセスされたときに this.getOrSet() によって遅延インスタンス化されます。
  • クライアントは薄いデータアクセス用ラッパーとして保ちます。ビジネスロジックはミドルウェアまたはサービス関数に記述します。

クライアントの階層:

クラス用途ベース URL
JanusClientVTEX の内部 API(Janus ゲートウェイ)にアクセスするhttps://{account}.vtexcommercestable.com.br
ExternalClient外部(VTEX 以外の)API にアクセスする指定した任意の URL
AppClient他の VTEX IO アプリが公開するルートにアクセスするhttps://{workspace}--{account}.myvtex.com
InfraClientVTEX IO インフラストラクチャサービスにアクセスする内部
MasterDataClientMaster Data v2 の CRUD 操作にアクセスするVTEX API

アーキテクチャ:

Request → VTEX IO Runtime → Service
  ├── routes → middleware chain → ctx.clients.{name}.method()
  ├── graphql → resolvers → ctx.clients.{name}.method()
  └── events → handlers → ctx.clients.{name}.method()
                               │
                               ▼
                         Client (JanusClient / ExternalClient)
                               │
                               ▼
                    External Service / VTEX API

厳守事項

制約:@vtex/api Clients を使用し、生の HTTP ライブラリは使用しない

VTEX IO サービスアプリからのすべての HTTP 通信は、必ず @vtex/api クライアント(JanusClient、ExternalClient、AppClient、または @vtex/clients のネイティブクライアント)を経由する必要があります。axios、fetch、got、node-fetch、その他の生の HTTP ライブラリを使用してはいけません。

重要な理由

VTEX IO クライアントでは、認証ヘッダーの自動挿入、組み込みのキャッシュ(ディスクおよびメモリ)、指数バックオフによる再試行、タイムアウト管理、ネイティブのメトリクスと課金の追跡、適切なエラー処理が提供されます。生の HTTP ライブラリでは、これらすべてが回避されてしまいます。また、VTEX IO からのアウトバウンド通信にはファイアウォールが適用されているため、インフラストラクチャを介して正しくルーティングできるのは @vtex/api クライアントだけです。

検出方法

VTEX IO サービスアプリ内で import axios from 'axios'、import fetch from 'node-fetch'、import got from 'got'、require('node-fetch')、または fetch() の直接呼び出しを見つけた場合は、そこで作業を止めてください。JanusClient または ExternalClient を拡張する適切なクライアントに置き換えてください。

正しい例

import type { InstanceOptions, IOContext } from '@vtex/api'
import { ExternalClient } from '@vtex/api'

export class WeatherClient extends ExternalClient {
  constructor(context: IOContext, options?: InstanceOptions) {
    super('https://api.weather.com', context, {
      ...options,
      headers: {
        'X-Api-Key': 'my-key',
        ...options?.headers,
      },
    })
  }

  public async getForecast(city: string): Promise<Forecast> {
    return this.http.get(`/v1/forecast/${city}`, {
      metric: 'weather-forecast',
    })
  }
}

誤った例

import axios from 'axios'

// This bypasses VTEX IO infrastructure entirely.
// No caching, no retries, no metrics, no auth token injection.
// Outbound requests may be blocked by the firewall.
export async function getForecast(city: string): Promise<Forecast> {
  const response = await axios.get(`https://api.weather.com/v1/forecast/${city}`, {
    headers: { 'X-Api-Key': 'my-key' },
  })
  return response.data
}

制約:クライアントには ctx.clients 経由でアクセスする — 直接インスタンス化しない

ミドルウェア、リゾルバー、イベントハンドラーでは、必ず ctx.clients.{clientName} 経由でクライアントにアクセスしてください。new を使ってクライアントクラスを直接インスタンス化してはいけません。

重要な理由

IOClients レジストリはクライアントのライフサイクルを管理し、現在のリクエストの IOContext(アカウント、ワークスペース、認証トークン)を使って適切に初期化します。直接インスタンス化すると、認証コンテキスト、キャッシュ設定、メトリクスパイプラインとの接続がないクライアントが作成されます。

検出方法

ミドルウェアまたはリゾルバー内に new MyClient(...) または new ExternalClient(...) があれば、処理を止めてください。クライアントは Clients クラスに登録し、ctx.clients 経由でアクセスする必要があります。

正しい例

// node/clients/index.ts
import { IOClients } from '@vtex/api'
import { CatalogClient } from './catalogClient'

export class Clients extends IOClients {
  public get catalog() {
    return this.getOrSet('catalog', CatalogClient)
  }
}

// node/middlewares/getProduct.ts
export async function getProduct(ctx: Context, next: () => Promise<void>) {
  const { clients: { catalog } } = ctx
  const product = await catalog.getProductById(ctx.query.id)
  ctx.body = product
  ctx.status = 200
  await next()
}

誤った例

// node/middlewares/getProduct.ts
import { CatalogClient } from '../clients/catalogClient'

export async function getProduct(ctx: Context, next: () => Promise<void>) {
  // Direct instantiation — no auth context, no caching, no metrics
  const catalog = new CatalogClient(ctx.vtex, {})
  const product = await catalog.getProductById(ctx.query.id)
  ctx.body = product
  ctx.status = 200
  await next()
}

制約:モノリシックなサービスアプリを避ける

1 つのサービスアプリで定義する HTTP ルートは 10 個以下にしてください。さらに必要な場合は、用途に特化したマイクロサービスアプリへの分割を検討してください。

重要な理由

VTEX IO アプリはメモリに制限のあるコンテナー(最大 512MB)で実行されます。ルートの多いモノリシックなアプリは、メモリ使用量、コールドスタート時間、障害の影響範囲を増大させます。VTEX IO プラットフォームは、連携する小規模で用途に特化したアプリ向けに設計されています。

検出方法

service.json に 10 個を超えるルートが定義されている場合は、アプリをより小規模なサービスに分割することを検討するよう開発者に警告してください。これはソフトリミットであり、妥当な例外もあります。

正しい例

{
  "memory": 256,
  "timeout": 30,
  "routes": {
    "get-reviews": { "path": "/_v/api/reviews", "public": false },
    "get-review": { "path": "/_v/api/reviews/:id", "public": false },
    "create-review": { "path": "/_v/api/reviews", "public": false },
    "moderate-review": { "path": "/_v/api/reviews/:id/moderate", "public": false }
  }
}

誤った例

{
  "memory": 512,
  "timeout": 60,
  "routes": {
    "route1": { "path": "/_v/api/reviews" },
    "route2": { "path": "/_v/api/reviews/:id" },
    "route3": { "path": "/_v/api/products" },
    "route4": { "path": "/_v/api/products/:id" },
    "route5": { "path": "/_v/api/orders" },
    "route6": { "path": "/_v/api/orders/:id" },
    "route7": { "path": "/_v/api/users" },
    "route8": { "path": "/_v/api/users/:id" },
    "route9": { "path": "/_v/api/categories" },
    "route10": { "path": "/_v/api/categories/:id" },
    "route11": { "path": "/_v/api/brands" },
    "route12": { "path": "/_v/api/inventory" }
  }
}

レビュー、商品、注文、ユーザー、カテゴリー、ブランド、在庫を網羅する 12 個のルートは、3~4 個の別々のアプリに分割する必要があります。

推奨パターン

カスタムクライアントを定義します。

// node/clients/catalogClient.ts
import type { InstanceOptions, IOContext } from '@vtex/api'
import { JanusClient } from '@vtex/api'

export class CatalogClient extends JanusClient {
  constructor(context: IOContext, options?: InstanceOptions) {
    super(context, {
      ...options,
      headers: {
        VtexIdclientAutCookie: context.authToken,
        ...options?.headers,
      },
    })
  }

  public async getProduct(productId: string): Promise<Product> {
    return this.http.get(`/api/catalog/pvt/product/${productId}`, {
      metric: 'catalog-get-product',
    })
  }

  public async listSkusByProduct(productId: string): Promise<Sku[]> {
    return this.http.get(`/api/catalog_system/pvt/sku/stockkeepingunitByProductId/${productId}`, {
      metric: 'catalog-list-skus',
    })
  }
}

IOClients にクライアントを登録します。

// node/clients/index.ts
import { IOClients } from '@vtex/api'
import { CatalogClient } from './catalogClient'
import { ReviewStorageClient } from './reviewStorageClient'

export class Clients extends IOClients {
  public get catalog() {
    return this.getOrSet('catalog', CatalogClient)
  }

  public get reviewStorage() {
    return this.getOrSet('reviewStorage', ReviewStorageClient)
  }
}

ctx.clients と ctx.state を使用してミドルウェアを作成します。

// node/middlewares/getReviews.ts
import type { ServiceContext } from '@vtex/api'
import type { Clients } from '../clients'

type Context = ServiceContext<Clients>

export async function validateParams(ctx: Context, next: () => Promise<void>) {
  const { productId } = ctx.query

  if (!productId || typeof productId !== 'string') {
    ctx.status = 400
    ctx.body = { error: 'productId query parameter is required' }
    return
  }

  ctx.state.productId = productId
  await next()
}

export async function getReviews(ctx: Context, next: () => Promise<void>) {
  const { productId } = ctx.state
  const reviews = await ctx.clients.reviewStorage.getByProduct(productId)

  ctx.status = 200
  ctx.body = reviews
  await next()
}

Service のエントリーポイントですべてを接続します。

// node/index.ts
import type { ParamsContext, RecorderState } from '@vtex/api'
import { Service, method } from '@vtex/api'

import { Clients } from './clients'
import { validateParams, getReviews } from './middlewares/getReviews'
import { createReview } from './middlewares/createReview'
import { resolvers } from './resolvers'

export default new Service<Clients, RecorderState, ParamsContext>({
  clients: {
    implementation: Clients,
    options: {
      default: {
        retries: 2,
        timeout: 5000,
      },
      catalog: {
        retries: 3,
        timeout: 10000,
      },
    },
  },
  routes: {
    reviews: method({
      GET: [validateParams, getReviews],
      POST: [createReview],
    }),
  },
  graphql: {
    resolvers: {
      Query: resolvers.queries,
      Mutation: resolvers.mutations,
    },
  },
})

よくある失敗例

  • HTTP 呼び出しに axios/fetch/got/node-fetch を使用する:これらのライブラリは VTEX IO のインフラストラクチャ全体を迂回します。認証トークンの自動挿入、キャッシュ、再試行ロジック、メトリクスはいずれも利用できません。また、ファイアウォールによって外部へのリクエストがブロックされる場合もあります。代わりに、ExternalClient または JanusClient を拡張した適切なクライアントを作成してください。
  • クライアントにビジネスロジックを記述する:クライアントが肥大化し、テストが困難になります。クライアントは HTTP 呼び出しを行う薄いラッパーにとどめてください。ビジネスロジックはミドルウェアまたは専用のサービス関数に記述します。
  • クライアントを直接インスタンス化する:ミドルウェア内で new MyClient(...) を使うと、認証コンテキスト、キャッシュ、メトリクスのないクライアントが作成されます。必ず ctx.clients 経由でアクセスしてください。

レビューチェックリスト

  • すべての HTTP 呼び出しが @vtex/api クライアント経由ですか(axios、fetch、got は使用していませんか)?
  • すべてのクライアントに ctx.clients 経由でアクセスし、new でインスタンス化していませんか?
  • カスタムクライアントは IOClients クラスに登録されていますか?
  • Service のエントリーポイントでは、クライアント、ルート、リゾルバー、イベントが正しく接続されていますか?
  • ビジネスロジックはクライアントクラスではなく、ミドルウェア/リゾルバーに記述されていますか?
  • service.json のルート数は適切ですか(≤10)?
  • クライアントのオプション(再試行、タイムアウト)は適切に設定されていますか?

参考資料

コメント