vtex-io-service-apps について
VTEX IOアプリにバックエンドロジックを実装する開発者向けのスキルです。サービスのエントリーポイント、ルート、ミドルウェア、イベントハンドラー、登録済みAPIクライアントを説明し、VTEX基盤に沿った実装方法を示します。
主な機能
- 01node/index.tsによるサービス構成
- 02ルートとミドルウェアチェーンの設定
- 03APIクライアントの作成と登録
- 04ctx.clients経由のクライアント利用
- 05リゾルバーとイベントの処理
- 06HTTP呼び出しとルート構成の確認
活用シーン
- 01VTEX IOアプリのREST APIルート構築
- 02VTEXまたは外部APIへの接続
- 03GraphQLリゾルバーやイベントハンドラーの追加
- 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 |
|---|---|---|
JanusClient | VTEX の内部 API(Janus ゲートウェイ)にアクセスする | https://{account}.vtexcommercestable.com.br |
ExternalClient | 外部(VTEX 以外の)API にアクセスする | 指定した任意の URL |
AppClient | 他の VTEX IO アプリが公開するルートにアクセスする | https://{workspace}--{account}.myvtex.com |
InfraClient | VTEX IO インフラストラクチャサービスにアクセスする | 内部 |
MasterDataClient | Master 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)? - クライアントのオプション(再試行、タイムアウト)は適切に設定されていますか?
参考資料
- サービス — VTEX IO のバックエンドサービス開発の概要
- クライアント — ネイティブクライアントの一覧とクライアントアーキテクチャの概要
- クライアントの開発 — カスタムの JanusClient と ExternalClient を作成するための手順ガイド
- Node クライアントの使用 — ミドルウェアとリゾルバーで @vtex/api と @vtex/clients を使用する方法
- Commerce API の呼び出し — VTEX Commerce API を呼び出すサービスアプリを構築するチュートリアル
- レート制限を回避するためのベストプラクティス — キャッシュを備えたクライアントがレート制限の問題を防ぐ仕組み