mcpField
V
vtex-io-graphql-api

vtex-io-graphql-api

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

vtex-io-graphql-api について

VTEX IO で GraphQL API を構築する開発スキルです。スキーマと TypeScript リゾルバーのほか、認証、キャッシュ、VTEX データへのアクセスを扱います。

主な機能

  1. 01GraphQL スキーマの定義
  2. 02TypeScript リゾルバーの実装
  3. 03認証とキャッシュディレクティブの設定
  4. 04スキーマフィールドへのリゾルバー登録
  5. 05VTEX クライアントによるデータアクセス

活用シーン

  1. 01ストアフロントコンポーネントへのデータ提供
  2. 02他の VTEX IO アプリ向け API の提供
  3. 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 を使用してください)

判断ルール

  • graphql builder は /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 連携

参考資料

コメント