vtex-io-service-runtime について
VTEX IO バックエンド開発者向けのランタイムスキルです。サービスのエントリーポイント、型契約、ルートやイベントの登録、`service.json` の設定を扱い、新規構築やランタイム配線の問題解決に役立ちます。
主な機能
- 01node/index.ts エントリーポイントの構成
- 02ルート、イベント、GraphQL ハンドラーの登録
- 03Context、State、パラメーターの型定義
- 04service.json でのランタイム設定
- 05ルート公開範囲の明示
活用シーン
- 01VTEX IO バックエンドアプリの新規構築
- 02サービス入口の責務分担の確認
- 03タイムアウト、メモリー、レプリカの設定
- 04ルートやイベント登録の問題調査
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vtex/ skills --skill vtex-io-service-runtimeインストール後、Claude Code で/vtex-io-service-runtimeを入力して読み込まれたか確認します。
詳細
サービスのランタイムと実行モデル
このスキルを使用する場面
メインとなる判断が、VTEX IO バックエンドアプリが node ビルダー内でどのように実行されるかに関する場合に、このスキルを使用してください。具体的には、Service エントリーポイントの構成方法、ランタイム設定の宣言方法、ルート、イベント、GraphQL ハンドラーをサービスに登録する方法が該当します。
node/の下に新しいバックエンドアプリを作成する場合node/index.tsをサービスのエントリーポイントとして構成する場合- ハンドラー用の
Context、State、パラメーターの型付きコントラクトを定義する場合 - タイムアウト、メモリ、ワーカー、レプリカを設定するために
service.jsonを構成する場合 - サービス登録や実行モデルの不一致によって発生するランタイムの問題をトラブルシューティングする場合
- ランタイムレベルで GraphQL ハンドラーを登録する場合。ただし、スキーマとリゾルバーの設計には別のスキルを使用してください
次の用途には、このスキルを使用しないでください。
manifest.jsonでアプリのコントラクトを決定すること- カスタムクライアントや統合トランスポート層を設計すること
- HTTP ルートハンドラーの詳細な動作を扱うこと
- イベント固有のビジネスワークフローを扱うこと
- ランタイム登録を超えて GraphQL スキーマやリゾルバーをモデル化すること
判断ルール
node/index.tsは、バックエンドアプリのランタイム構成ルートとして扱ってください。Service定義は、ルート、イベント、GraphQL ハンドラーなどのランタイム機能を登録するために使用し、ビジネスロジックを直接格納しないでください。- ランタイムの配線は明示的にしてください。コンテキストの型指定、クライアントの型指定、ルート登録、イベント登録が、サービス境界で確認できるようにします。
- タイムアウト、ttl、メモリ、ワーカー、レプリカ数などの実行パラメーターは、ハンドラーのコード内ではなく
service.jsonに記述してください。 - サービスの実行にプラットフォームが使用するランタイムパラメーターは、
service.jsonで宣言してください。特に、memory、timeout、ttl、minReplicas、maxReplicas、workers、routes、events、rateLimitPerReplicaを設定します。 - HTTP エントリーポイントを公開するには、
service.jsonのroutesを使用してください。ルートはデフォルトで非公開のため、外部からアクセス可能にするルートにはpublic: trueを明示的に設定してください。 smartcache: trueは、同じレスポンスを繰り返しのリクエストで安全に再利用できる、冪等でキャッシュ可能なルートにのみ使用してください。パーソナライズされたエンドポイント、認証が必要なエンドポイント、書き込みを行うエンドポイントでは使用しないでください。- サービスのランタイムに含めるイベントソースとハンドラーは、
service.jsonのeventsで宣言してください。イベント登録はランタイム層に置き、イベント固有のビジネスルールは専用のイベントモジュールに置いてください。 - リクエストやイベントのレプリカごとのスループットを調整するには、
rateLimitPerReplicaを使用してください。サービスで必要な場合にのみ全体の基本値を設定し、コストの高いルートや負荷の大きいイベントソースに限って、小規模な明示的オーバーライドを追加してください。 - コストの高いルートの再設計、作業のキューイング、処理の遅い操作の非同期処理への移行の代わりとして、
rateLimitPerReplicaを使用しないでください。 - ハンドラーはリクエストまたはイベントの処理に集中させ、ランタイム構成は起動と登録に集中させてください。
Context、State、パラメーターの型を意図的に設計し、ミドルウェアとハンドラーが安定したコントラクトを共有できるようにしてください。ミドルウェアにも同じ型付きのContextとStateを適用し、anyに頼らずにctx.state、ctx.vtex、パラメーターを安全に操作できるようにします。- バックエンドアプリでランタイムの配線、クライアントの実装、ビジネスルールが同じファイルに混在し始めたら、サービスをさらに拡張する前に、それぞれの関心事を分離してください。
routes.accessやroutes.policiesなど、一部の認可フィールドはservice.jsonに記述できますが、主に認可に関する設定です。ランタイムスキルではなく、認証やセキュリティに特化したスキルで扱ってください。
ランタイムのリソース設定の目安:
- これらの範囲は、パートナー向けおよびアカウントレベルのアプリを対象としています。VTEX のネイティブコアサービスでは、メモリが数千 MB、レプリカ数が数百といった、はるかに高い値を正当に使用する場合がありますが、カスタムアプリのデフォルト値としては使用しないでください。
推奨されるデフォルト値:
timeoutを 10〜30 秒に設定して、同期 HTTP サービスを開始します。UX に関わるルートでは、5〜15 秒を優先してください。memoryを 256 MB に設定して開始します。workersを 1 に設定して開始します。- インストール済みアプリのデフォルトには
minReplicas: 2を使用し、プラットフォームが許可する場合に限り、リンク済みアプリの開発コンテキストでminReplicas: 1を使用します。 - ドキュメントに記載された最小値が
5であるため、実用上の最低ラインとしてmaxReplicas: 5を使用します。 ttlは意図的に設定してください。VTEX IO では、ttlは分単位で計測され、プラットフォームのデフォルト値と上限はtimeoutとは異なります。パートナーアプリでは、デフォルトの10分を起点とし、アイドル状態のインスタンスを早くスリープさせることよりもコールドスタートの削減を重視する場合に限り、意図的に最大60分まで増やしてください。
スケーリングの範囲と例外:
- 単純な IO バウンドサービスでは 128〜256 MB を使用し、OOM、大きなペイロードの処理、またはより重いライブラリの使用を示す根拠がある場合に限り、512 MB に増やします。
- 効果を測定したうえで、高スループットの IO バウンドワークロードに限り、
workersを 2〜4 に増やします。デフォルトでインスタンスあたり 4 を超えるワーカーを使用しないでください。 - 公開トラフィックまたは予測可能なピークに必要な場合に限り、
maxReplicasを5から10に向けて増やします。10 を超える値は例外として扱い、パートナーアプリでは明確な根拠と監視を必須としてください。 - HTTP ルートでは、
timeoutを 60 秒より長く設定しないでください。さらに時間が必要な場合は、フローを非同期処理として再設計してください。 ttlのドキュメントに記載された最小値は10分、最大値は60分です。トラフィックの少ないサービスやトラフィックが急増するサービスのコールドスタートを減らす場合に限り、高い値を意図的に使用し、ttlをリクエストごとのタイムアウトのように扱わないでください。- パートナーアプリでは、通常のルートの
rateLimitPerReplica.perMinuteは 60〜300、高コストなルートでは 10〜60 から始めることがよくあります。rateLimitPerReplica.concurrentは 1〜5 から始めることがよくあります。
厳守すべき制約
制約:Service のエントリーポイントはランタイム構成ルートであり続ける必要があります
node/index.ts は、ビジネスロジック、データ変換、またはトランスポート実装を何でも詰め込むファイルではなく、VTEX IO サービスのランタイム構造を定義してエクスポートする必要があります。
重要な理由
エントリーポイントに登録処理とビジネスロジックが混在すると、実行モデルの把握が難しくなり、ハンドラーの結合度が高まり、ルート、イベント、または GraphQL の公開面を変更するリスクが増します。
検出方法
node/index.ts に大きなハンドラー本体、外部 API 呼び出し、複雑な分岐、またはデータマッピングロジックが含まれている場合は、作業を中断して、そのロジックを専用のモジュールに移してください。エントリーポイントは型定義と登録に専念させます。
適切な例
import type { ClientsConfig, RecorderState, ServiceContext } from '@vtex/api'
import { Service } from '@vtex/api'
import { clients, Clients } from './clients'
import { routes } from './routes'
export interface State extends RecorderState {}
export type Context = ServiceContext<Clients, State>
const clientsConfig: ClientsConfig<Clients> = {
implementation: clients,
options: {},
}
export default new Service<Clients, State>({
clients: clientsConfig,
routes,
})
不適切な例
import { Service } from '@vtex/api'
import axios from 'axios'
export default new Service({
routes: {
reviews: async (ctx: any) => {
const response = await axios.get('https://example.com/data')
const transformed = response.data.items.map((item: any) => ({
...item,
extra: true,
}))
ctx.body = transformed.filter((item: any) => item.active)
},
},
})
制約:ランタイム設定はコード内で独自に定義せず、service.json で表現する
アプリがタイムアウト、TTL、メモリ、ワーカー、レプリカの動作に依存する場合、リソースと実行に関する設定はservice.jsonで行う必要があります。
service.jsonはnode/フォルダー内にあり、このサービスのルート、イベント、メモリ、タイムアウト、TTL、ワーカー、レプリカ、レート制限などのランタイムパラメーターを一元管理します。
重要な理由
これらの設定は、プラットフォームとサービスのランタイム契約の一部です。設定を前提として隠したり、コード全体に分散させたりすると、動作の予測が難しくなり、タイムアウト、コールドスタートの頻発、リソース不足、スケーリングの不整合を引き起こす可能性があります。VTEX IOでは、TTLが特に重要です。単位は分であり、リクエストの合間にサービスインフラをどの程度積極的にアイドル状態にできるかに影響します。 トラフィックの少ないサービスで最小のTTLを使用すると、バースト間にプラットフォームがサービスをより積極的にスケールダウンできるため、コールドスタートが増える可能性があります。
検出
アプリが長時間実行される処理、同時実行、ウォーム状態のキャパシティ、または特定のルート公開動作に依存する場合は、作業を中断し、該当するservice.jsonの設定が存在し、意図した内容になっていることを確認してください。動作がコードコメントやハンドラーのロジックで暗示されているだけの場合は、ランタイム設定に移してください。
正しい例
{
"memory": 256,
"timeout": 30,
"ttl": 10,
"minReplicas": 2,
"maxReplicas": 10,
"workers": 4,
"rateLimitPerReplica": {
"perMinute": 300,
"concurrent": 10
},
"routes": {
"reviews": {
"path": "/_v/api/reviews",
"public": false
}
}
}
誤った例
{
"routes": {
"reviews": {
"path": "/_v/api/reviews"
}
}
}
このランタイム設定は、明示的なタイムアウト、同時実行、レート制限、またはレプリカの動作に依存するサービスには不十分であり、実行特性が未定義のままになっています。
制約:ルートの公開範囲はランタイム契約で明示する
サービスが公開するすべてのHTTPルートは、意図した公開範囲の設定とともにservice.jsonで宣言する必要があります。ルートを非公開または公開にする必要がある場合、暗黙のデフォルトに頼らないでください。
ルートはデフォルトで非公開です。そのため、外部からアクセス可能にする必要がある場合は、必ずpublic: trueを明示的に設定してください。
重要な理由
ルートの公開範囲は、サービスのランタイム契約の一部です。公開範囲が曖昧だと、ルートが意図とは異なるアクセス範囲で公開される可能性があり、非公開ハンドラーのセキュリティリスクや、公開を想定したルートの統合失敗につながります。
検出
サービスのランタイムにルートが存在する場合は、作業を中断し、service.jsonで宣言されていることと、publicが意図した公開範囲に合っていることを確認してください。信頼されたバックオフィスやアプリ間のフローでのみ使用されるルートの場合は、アクセス範囲を広げる前に、非公開になっていることを確認してください。
正しい例
{
"routes": {
"status": {
"path": "/_v/status/health",
"public": true,
"smartcache": true
},
"reviews": {
"path": "/_v/api/reviews",
"public": false
}
}
}
誤った例
{
"routes": {
"reviews": {
"path": "/_v/api/reviews"
}
}
}
このルートでは公開範囲が明示されていないため、エンドポイントを公開するのか保護するのかがランタイム契約から明確にわかりません。
制約:型付きのコンテキストと状態は、ランタイムに登録されたハンドラーと一致させる
サービスでは、登録するルート、イベント、GraphQLハンドラーに合致するContext、State、およびハンドラーの契約を定義する必要があります。
重要な理由
型付けされていない、または一貫性のないランタイム契約は、ミドルウェアの合成を不安定にし、存在が保証されていない状態やパラメーターにハンドラーが依存する原因になります。
検出
ミドルウェアやハンドラーが、共有された型付き契約なしにctx.state、ctx.clients、ctx.vtex、またはパラメーターフィールドを使用している場合は、作業を中断し、ハンドラーを追加する前にランタイムの型を導入するか修正してください。
正しい例
import type { ParamsContext, RecorderState, ServiceContext } from '@vtex/api'
interface State extends RecorderState {
reviewId?: string
}
type CustomContext = ServiceContext<Clients, State, ParamsContext>
export async function getReview(ctx: CustomContext) {
ctx.state.reviewId = ctx.vtex.route.params.id
ctx.body = { id: ctx.state.reviewId }
}
誤った例
export async function getReview(ctx: any) {
ctx.state.reviewId = ctx.params.review
ctx.body = { id: ctx.state.missingField.value }
}
推奨パターン
推奨するファイル構成:
node/
├── index.ts
├── clients/
│ └── index.ts
├── routes/
│ └── index.ts
├── events/
│ └── index.ts
├── graphql/
│ └── index.ts
└── middlewares/
└── validate.ts
最小限のサービスランタイムパターン:
import type { ClientsConfig, RecorderState, ServiceContext } from '@vtex/api'
import { Service } from '@vtex/api'
import { clients, Clients } from './clients'
import { routes } from './routes'
export interface State extends RecorderState {}
export type Context = ServiceContext<Clients, State>
const clientsConfig: ClientsConfig<Clients> = {
implementation: clients,
options: {},
}
export default new Service<Clients, State>({
clients: clientsConfig,
routes,
})
最小限のservice.jsonパターン:
{
"memory": 256,
"timeout": 30,
"ttl": 10,
"minReplicas": 2,
"maxReplicas": 5,
"workers": 1,
"rateLimitPerReplica": {
"perMinute": 120,
"concurrent": 4
},
"routes": {
"status": {
"path": "/_v/status/health",
"public": true,
"smartcache": true
},
"reviews": {
"path": "/_v/api/reviews",
"public": false
}
},
"events": {
"orderCreated": {
"sender": "vtex.orders-broadcast",
"topics": ["order-created"],
"rateLimitPerReplica": {
"perMinute": 60,
"concurrent": 2
}
}
}
}
サービスのエントリーポイントでランタイムの各要素を組み合わせ、ビジネスロジックはハンドラー、クライアント、その他の役割に特化したモジュールに配置してください。
routes/index.tsやevents/index.tsが大きくなりすぎた場合は、routes/orders.tsやevents/catalog.tsのようにドメインごとに分割し、インデックスファイルは小さなレジストリーとして保ってください。
よくある失敗例
- ビジネスロジックを
node/index.tsに直接記述する。 - 実行時の動作が明示的なリソース設定に依存しているにもかかわらず、
service.jsonを任意のものとして扱う。 ttlを低く設定しすぎて、トラフィックが集中する時間帯の合間にサービスが過度にスリープする。- パーソナライズされたルートや書き込み中心のルートで
smartcacheを有効にし、リクエスト間で誤ったキャッシュが再利用されるリスクを生じさせる。 - 明確に型付けされた
ContextとStateがないまま、ルート、イベント、GraphQL ハンドラーを登録する。 - 実行時の構成とクライアント実装の詳細を混在させる。
- HTTP、イベント、GraphQL にまたがる無関係な責務を、明確なモジュール境界なしに 1 つのサービスエントリーポイントに蓄積させる。
レビューチェックリスト
-
node/index.tsはビジネスロジックのファイルではなく、実行時の構成ルートとして機能していますか? - ルート、イベント、GraphQL ハンドラーは明示的かつ明確に登録されていますか?
-
service.jsonは、アプリが実際に依存する実行時の動作を表現していますか? -
Context、State、params の型は、すべてのハンドラーで一貫して共有されていますか? - 実行時の関心事は、クライアント実装やビジネスロジックから分離されていますか?
リファレンス
- サービス - VTEX IO サービスの実行時構造と登録
- Service JSON - VTEX IO サービスの実行時設定
- Node Builder -
nodeビルダー配下のバックエンドアプリ構造 - アプリの開発 - バックエンドアプリ開発の一般的な流れ