vtex-io-graphql-api について
VTEX IO で GraphQL API を構築する開発スキルです。スキーマと TypeScript リゾルバーのほか、認証、キャッシュ、VTEX データへのアクセスを扱います。
主な機能
- 01GraphQL スキーマの定義
- 02TypeScript リゾルバーの実装
- 03認証とキャッシュディレクティブの設定
- 04スキーマフィールドへのリゾルバー登録
- 05VTEX クライアントによるデータアクセス
活用シーン
- 01ストアフロントコンポーネントへのデータ提供
- 02他の VTEX IO アプリ向け API の提供
- 03VTEX Commerce API データの集約
インストール
コマンドをコピーしてターミナルで実行します。
npx skills add https:/ / github.com/ vtex/ skills --skill vtex-io-graphql-apiインストール後、Claude Code で/vtex-io-graphql-apiを入力して読み込まれたか確認します。
詳細
GraphQL スキーマとリゾルバー
このスキルを使用する場面
VTEX IO アプリで GraphQL API を公開する必要がある場合に、このスキルを使用します。フロントエンドの React コンポーネントからのクエリ、ほかの VTEX IO アプリからの利用、VTEX Commerce API 上に構築するカスタムデータ集約レイヤーの実装が該当します。
/graphqlディレクトリ内の.graphqlファイルでスキーマを定義する/node/resolvers/内の TypeScript でリゾルバー関数を記述する@cacheControlディレクティブと@authディレクティブを設定する- リゾルバーを Service クラスに接続する
次の用途にはこのスキルを使用しないでください。
- バックエンドサービスの構造とクライアントシステム(代わりに
vtex-io-service-appsを使用してください) - Manifest と builder の設定(代わりに
vtex-io-app-structureを使用してください) - MasterData の連携に関する詳細(代わりに
vtex-io-masterdataを使用してください)
判断ルール
graphqlbuilder は/graphql内の.graphqlファイルを処理し、それらを単一のスキーマに統合します。- 保守性を高めるため、定義を複数のファイルに分割します。ルート型には
schema.graphql、ディレクティブ宣言にはdirectives.graphql、カスタム型にはtypes/*.graphqlを使用します。 - すべての公開 Query フィールドに
@cacheControl(scope: PUBLIC, maxAge: SHORT|MEDIUM|LONG)を使用します。PUBLICは共有 CDN キャッシュ、PRIVATEはユーザーごとのキャッシュです。 - すべての Mutation と、機密データまたはユーザー固有データを返す Query に
@authを使用します。 - Mutation には
@cacheControlを決して使用しないでください。 - Service のエントリーポイントにあるリゾルバー関数のキーは、
schema.graphql内のフィールド名と完全に一致させる必要があります。 - リゾルバーでのデータアクセスには必ず
ctx.clientsを使用し、生の HTTP 呼び出しは使用しないでください。
推奨ディレクトリ構造:
graphql/
├── schema.graphql # Query and Mutation root type definitions
├── directives.graphql # Custom directive declarations (@cacheControl, @auth)
└── types/
├── Review.graphql # Custom type definitions
└── Product.graphql # One file per type for organization
組み込みディレクティブ:
@cacheControl:scope(PUBLIC/PRIVATE)、maxAge(SHORTは 30 秒、MEDIUMは 5 分、LONGは 1 時間)@auth:有効な VTEX 認証トークンを要求します。このディレクティブがない場合、未認証ユーザーもエンドポイントを呼び出せます。@smartcache:VTEX のインフラストラクチャでクエリ結果を自動的にキャッシュします。
厳守事項
制約:graphql Builder を宣言する
.graphql スキーマファイルを使用するアプリは、manifest.json で graphql builder を宣言する必要があります。graphql builder はスキーマを解釈し、VTEX IO ランタイムに登録します。
重要な理由
graphql builder の宣言がないと、/graphql ディレクトリは完全に無視されます。スキーマファイルは処理されず、リゾルバーは登録されず、GraphQL クエリは「schema not found」エラーを返します。アプリはエラーなしでリンクされますが、GraphQL はエラーが表示されないまま動作しません。
検出方法
/graphql ディレクトリに .graphql ファイルがあるのに、manifest の builders に "graphql": "1.x" が含まれていない場合は、作業を中止して builder の宣言を追加してください。
正しい例
{
"builders": {
"node": "7.x",
"graphql": "1.x"
}
}
誤った例
{
"builders": {
"node": "7.x"
}
}
"graphql": "1.x" がないため、スキーマファイルがある /graphql ディレクトリは無視されます。スキーマが登録されないため、GraphQL クエリはエラーを返します。アプリは正常にリンクされるため、問題が見えにくくなります。
制約:公開 Query には @cacheControl を使用する
公開向け Query フィールド(ユーザー固有ではないデータを取得するフィールド)には、適切な scope と maxAge を指定した @cacheControl ディレクティブを含める必要があります。Mutation に @cacheControl を使用してはいけません。
重要な理由
@cacheControl がないと、リクエストのたびにすべてのクエリでリゾルバーが呼び出されます。CDN キャッシュ、エッジキャッシュ、共有キャッシュが利用されません。その結果、VTEX インフラストラクチャに不要な負荷がかかり、応答時間が遅くなり、レート制限に達する可能性があります。公開商品データでは、パフォーマンスを保つうえでキャッシュが不可欠です。
検出方法
Query フィールドが公開データ(ユーザー固有ではないデータ)を返し、@cacheControl が付いていない場合は、追加するよう開発者に警告してください。Mutation に @cacheControl が付いている場合は、作業を中止して削除してください。
正しい例
type Query {
reviews(productId: String!, limit: Int): [Review]
@cacheControl(scope: PUBLIC, maxAge: SHORT)
productMetadata(slug: String!): ProductMetadata
@cacheControl(scope: PUBLIC, maxAge: MEDIUM)
myReviews: [Review]
@cacheControl(scope: PRIVATE, maxAge: SHORT)
@auth
}
type Mutation {
createReview(review: ReviewInput!): Review @auth
}
誤った例
type Query {
reviews(productId: String!, limit: Int): [Review]
myReviews: [Review]
}
type Mutation {
createReview(review: ReviewInput!): Review
@cacheControl(scope: PUBLIC, maxAge: LONG)
}
クエリにキャッシュ制御がない(リクエストのたびにリゾルバーが呼び出される)、ユーザー固有データに @auth がない、Mutation に @cacheControl が付いている(意味がありません)。
制約:リゾルバー名はスキーマフィールド名と一致させる必要があります
Service のエントリポイントにあるリゾルバー関数のキーは、schema.graphql で定義されたフィールド名と完全に一致している必要があります。リゾルバーオブジェクトの構造は、GraphQL の型階層を反映していなければなりません。
重要な理由
GraphQL ランタイムは、名前を使って受信したクエリをリゾルバー関数にマッピングします。リゾルバーのキーがスキーマのフィールド名と一致しない場合、そのフィールドはエラーなしで null に解決されます。これはサイレントな失敗であり、デバッグが非常に困難です。
検出方法
スキーマフィールドに対応するリゾルバーキーがない場合(またはその逆の場合)は、作業を停止してください。node/index.ts のリゾルバー登録と、すべての Query フィールドおよび Mutation フィールドを照合してください。
正しい例
type Query {
reviews(productId: String!): [Review]
reviewById(id: ID!): Review
}
// node/index.ts — resolver keys match schema field names exactly
export default new Service({
graphql: {
resolvers: {
Query: {
reviews: reviewsResolver,
reviewById: reviewByIdResolver,
},
},
},
})
誤った例
// node/index.ts — resolver key "getReviews" does not match schema field "reviews"
export default new Service({
graphql: {
resolvers: {
Query: {
getReviews: reviewsResolver, // Wrong! Schema says "reviews", not "getReviews"
getReviewById: reviewByIdResolver, // Wrong! Schema says "reviewById"
},
},
},
})
両方のフィールドは、エラーなしで null に解決されます。ログにもエラーは記録されません。
推奨パターン
GraphQL ビルダーをマニフェストに追加します。
{
"builders": {
"node": "7.x",
"graphql": "1.x"
}
}
スキーマを定義します。
type Query {
reviews(productId: String!, limit: Int, offset: Int): ReviewsResponse
@cacheControl(scope: PUBLIC, maxAge: SHORT)
review(id: ID!): Review
@cacheControl(scope: PUBLIC, maxAge: SHORT)
}
type Mutation {
createReview(input: ReviewInput!): Review @auth
updateReview(id: ID!, input: ReviewInput!): Review @auth
deleteReview(id: ID!): Boolean @auth
}
カスタム型を定義します。
type Review {
id: ID!
productId: String!
author: String!
rating: Int!
title: String!
text: String!
createdAt: String!
approved: Boolean!
}
type ReviewsResponse {
data: [Review!]!
total: Int!
hasMore: Boolean!
}
input ReviewInput {
productId: String!
rating: Int!
title: String!
text: String!
}
ディレクティブを宣言します。
directive @cacheControl(
scope: CacheControlScope
maxAge: CacheControlMaxAge
) on FIELD_DEFINITION
enum CacheControlScope {
PUBLIC
PRIVATE
}
enum CacheControlMaxAge {
SHORT
MEDIUM
LONG
}
directive @auth on FIELD_DEFINITION
directive @smartcache on FIELD_DEFINITION
リゾルバーを実装します。
// node/resolvers/reviews.ts
import type { ServiceContext } from '@vtex/api'
import type { Clients } from '../clients'
type Context = ServiceContext<Clients>
export const queries = {
reviews: async (
_root: unknown,
args: { productId: string; limit?: number; offset?: number },
ctx: Context
) => {
const { productId, limit = 10, offset = 0 } = args
const reviews = await ctx.clients.masterdata.searchDocuments<Review>({
dataEntity: 'reviews',
fields: ['id', 'productId', 'author', 'rating', 'title', 'text', 'createdAt', 'approved'],
where: `productId=${productId} AND approved=true`,
pagination: { page: Math.floor(offset / limit) + 1, pageSize: limit },
schema: 'review-schema-v1',
})
return {
data: reviews,
total: reviews.length,
hasMore: reviews.length === limit,
}
},
review: async (
_root: unknown,
args: { id: string },
ctx: Context
) => {
return ctx.clients.masterdata.getDocument<Review>({
dataEntity: 'reviews',
id: args.id,
fields: ['id', 'productId', 'author', 'rating', 'title', 'text', 'createdAt', 'approved'],
})
},
}
export const mutations = {
createReview: async (
_root: unknown,
args: { input: ReviewInput },
ctx: Context
) => {
const { input } = args
const documentResponse = await ctx.clients.masterdata.createDocument({
dataEntity: 'reviews',
fields: {
...input,
author: ctx.vtex.storeUserEmail ?? 'anonymous',
approved: false,
createdAt: new Date().toISOString(),
},
schema: 'review-schema-v1',
})
return ctx.clients.masterdata.getDocument<Review>({
dataEntity: 'reviews',
id: documentResponse.DocumentId,
fields: ['id', 'productId', 'author', 'rating', 'title', 'text', 'createdAt', 'approved'],
})
},
deleteReview: async (
_root: unknown,
args: { id: string },
ctx: Context
) => {
await ctx.clients.masterdata.deleteDocument({
dataEntity: 'reviews',
id: args.id,
})
return true
},
}
リゾルバーを Service に接続します。
// node/index.ts
import type { ParamsContext, RecorderState } from '@vtex/api'
import { Service } from '@vtex/api'
import { Clients } from './clients'
import { queries, mutations } from './resolvers/reviews'
export default new Service<Clients, RecorderState, ParamsContext>({
clients: {
implementation: Clients,
options: {
default: {
retries: 2,
timeout: 5000,
},
},
},
graphql: {
resolvers: {
Query: queries,
Mutation: mutations,
},
},
})
リンク後に GraphQL API をテストします。
query GetReviews {
reviews(productId: "12345", limit: 5) {
data {
id
author
rating
title
text
createdAt
}
total
hasMore
}
}
mutation CreateReview {
createReview(input: {
productId: "12345"
rating: 5
title: "Excellent product"
text: "Really happy with this purchase."
}) {
id
author
createdAt
}
}
よくある失敗
- 対応するスキーマフィールドなしでリゾルバーを定義する:GraphQL ランタイムが公開するのは、スキーマで定義されたフィールドのみです。対応するフィールドがないリゾルバーは、通知なく無視されます。反対に、リゾルバーがないスキーマフィールドは
nullを返します。必ず先にスキーマを定義し、同じ名前の対応するリゾルバーを実装してください。 - リゾルバーから外部 API に直接クエリを送信する:
fetch()やaxiosを使うと、@vtex/apiのクライアントシステムを迂回するため、キャッシュ、再試行、メトリクス、認証が利用できなくなります。リゾルバーでは必ずctx.clientsを使用してください。 - Mutation エンドポイントに @auth がない:
@authがないと、匿名ユーザーでも Mutation を呼び出せてしまい、重大なセキュリティ上の脆弱性となります。Mutation と、機密データを返す Query には必ず@authを追加してください。 - 公開 Query に @cacheControl がない:キャッシュを使わずにすべてのリクエストがリゾルバーに到達するため、不要な負荷が発生し、応答が遅くなります。すべての公開 Query フィールドに適切なキャッシュディレクティブを追加してください。
レビューチェックリスト
-
graphqlビルダーがmanifest.jsonで宣言されていますか? - すべての公開 Query フィールドに、適切な scope と maxAge を指定した
@cacheControlがありますか? - すべての Mutation と機密情報を扱う Query に
@authがありますか? - リゾルバー関数のキーは、スキーマフィールド名と完全に一致していますか?
- リゾルバーはデータアクセスに
ctx.clientsを使用していますか(生の HTTP 呼び出しを行っていませんか)? -
directives.graphqlにディレクティブ宣言がありますか? - Service のエントリポイントで、リゾルバーが
graphql.resolversの下に接続されていますか?
関連スキル
vtex-io-service-apps— すべての GraphQL リゾルバーに必要な Service アプリの基礎vtex-io-app-contract— GraphQL が依存するマニフェストとビルダーの設定vtex-io-masterdata— リゾルバーのデータソースとしてよく使われる MasterData 連携
参考資料
- VTEX IO における GraphQL — VTEX IO プラットフォームでの GraphQL の使用方法の概要
- GraphQL Builder — スキーマ処理とディレクトリ構成に関する Builder のリファレンス
- Service Apps での GraphQL API の開発 — GraphQL API を構築するためのステップごとのチュートリアル
- GraphQL API とアプリの連携 — 他の VTEX IO アプリから GraphQL API を利用する方法
- IO アプリにおける GraphQL の認可 — 保護された GraphQL オペレーションに
@authディレクティブを実装して使用する方法 - IO アプリ向け GraphQL API のキャッシュ実装 — GraphQL オペレーションに
@cacheControlディレクティブを実装して使用する方法 - クライアント — データアクセスのためにリゾルバーで ctx.clients を使用する方法