mcpField
F
faststore-data-fetching

faststore-data-fetching

github.com/vtex开发4780 次安装2 周前更新
代码仓库

关于 faststore-data-fetching

面向 FastStore 项目的编码技能,指导目录数据获取与 GraphQL API 扩展,涵盖 VTEX 和第三方数据,并强调凭据应在服务端处理及扩展目录规范。

主要功能

  1. 01通过 FastStore GraphQL 获取目录数据
  2. 02扩展 VTEX 和第三方 API
  3. 03使用服务端与客户端查询片段
  4. 04在服务端处理密钥
  5. 05检查解析器结构与实现

适用场景

  1. 01为商品查询添加自定义字段
  2. 02集成评价或外部库存数据
  3. 03用服务端片段获取页面初始数据
  4. 04检查客户端代码中的不安全 API 调用

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/vtex/skills --skill faststore-data-fetching

安装后在 Claude Code 中输入/faststore-data-fetching确认已加载。

详细介绍

FastStore 数据层与 API 集成

适用场景

在以下情况下使用此技能:

  • 你需要获取产品数据、为现有查询添加更多字段,或集成第三方 API。
  • 你需要获取 FastStore 原生组件默认不会显示的数据。
  • 你要在 src/graphql/vtex/ 或 src/graphql/thirdParty/ 中创建 API 扩展。
  • 你要在 src/fragments/ 中添加 GraphQL 片段,以便在预定义查询中包含新字段。
  • 你要编写调用 VTEX REST API 或外部服务的服务端解析器。

以下情况不要使用此技能:

  • 客户端状态管理(购物车、会话、搜索)——使用 faststore-state-management 技能。
  • 视觉主题设置——使用 faststore-theming 技能。
  • 组件替换或 props 覆盖——使用 faststore-overrides 技能。

决策规则

  • 所有目录数据(产品、集合、搜索结果、价格)都使用 FastStore GraphQL API——切勿在客户端代码中直接调用 REST API。
  • 访问 VTEX 平台默认未公开的数据(例如自定义产品字段、分期付款详情)时,使用 VTEX API 扩展(src/graphql/vtex/)。
  • 集成第三方数据源(例如评价、评分、外部库存)时,使用第三方 API 扩展(src/graphql/thirdParty/)。
  • 对于页面加载时(SSR)所需的数据,使用服务端片段(src/fragments/ServerProduct.ts)。
  • 对于初始渲染后可加载的数据,使用客户端片段(src/fragments/ClientProduct.ts)。
  • API 密钥和机密信息仅保存在服务端解析器中——切勿放在客户端代码或 NEXT_PUBLIC_ 环境变量中。
  • 不要创建自定义 Next.js API 路由(pages/api/)——改用 API 扩展系统。

硬性约束

约束:目录数据必须使用 GraphQL 层

获取目录数据(产品、集合、搜索结果、价格)时,必须使用 FastStore GraphQL API。客户端代码不得直接调用 VTEX Catalog API(/api/catalog/、/api/catalog_system/)的 REST 接口。

原因 FastStore API 会处理身份验证、缓存、请求批处理和数据规范化。直接调用 REST 接口会绕过这些优化,并将 VTEX 域名结构暴露给浏览器。这还会导致 CORS 问题、重复的数据获取逻辑,并失去 GraphQL 提供的类型安全。在 GraphQL 解析器中从服务端调用 VTEX API 的 REST 接口是可以接受的——API 扩展正是为此而设。

检测 如果在客户端代码(组件、钩子、useEffect)中看到 fetch('https://{account}.vtexcommercestable.com.br/api/catalog') 或 fetch('https://{account}.myvtex.com/api/catalog') → 提醒这会绕过 GraphQL 层。如果它位于 src/graphql/ 下的解析器文件中 → 可以接受(这是 API 扩展模式)。如果在 src/components/ 或 src/pages/ 下的任何文件中看到使用 VTEX API 路径的 axios 或 fetch → 停止并重构为使用 GraphQL API。

正确

// src/graphql/vtex/resolvers/product.ts
// Server-side resolver — REST calls to VTEX APIs are correct here
import type { Resolver } from '@faststore/api'

const productResolver: Record<string, Resolver> = {
  StoreProduct: {
    customAttribute: async (root, _args, context) => {
      // Server-side: safe to call VTEX REST APIs in resolvers
      const response = await context.clients.commerce.catalog.getProduct(
        root.productID
      )
      return response.customAttribute
    },
  },
}

export default productResolver

错误

// src/components/ProductCustomData.tsx
// WRONG: Direct REST call to VTEX Catalog API from a client component
import React, { useEffect, useState } from 'react'

interface ProductCustomDataProps {
  productId: string
}

export default function ProductCustomData({ productId }: ProductCustomDataProps) {
  const [data, setData] = useState(null)

  useEffect(() => {
    // WRONG: Direct REST call from the browser
    // This exposes the VTEX domain, bypasses caching, and creates CORS issues.
    fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`)
      .then((res) => res.json())
      .then(setData)
  }, [productId])

  return <div>{data?.Name}</div>
}

限制:绝不在客户端代码中暴露 API 密钥

严禁在客户端代码、以 NEXT_PUBLIC_ 为前缀的环境变量或任何会被打包到浏览器中的文件中包含 VTEX API 密钥(VTEX_APP_KEY、VTEX_APP_TOKEN)或任何机密凭据。

重要原因 客户端代码中的 API 密钥会被任何查看页面源代码或网络请求的人看到。VTEX API 密钥可用于访问目录管理、订单处理和账户管理。密钥一旦暴露,就可能被用于修改商品、访问客户数据或扰乱店铺运营。这是严重的安全漏洞。

检测 如果在 src/components/、src/pages/ 下的任何文件,或任何会在浏览器中运行的文件中看到 VTEX_APP_KEY、VTEX_APP_TOKEN、X-VTEX-API-AppKey 或 X-VTEX-API-AppToken → 立即停止。这是严重的安全问题。如果在 .env 文件中看到 NEXT_PUBLIC_VTEX_APP_KEY 或 NEXT_PUBLIC_VTEX_APP_TOKEN → 立即停止。NEXT_PUBLIC_ 前缀会使这些值在浏览器包中可用。

正确

// src/graphql/vtex/resolvers/installments.ts
// API keys are used ONLY in server-side resolvers, accessed via context
import type { Resolver } from '@faststore/api'

const installmentResolver: Record<string, Resolver> = {
  StoreProduct: {
    availableInstallments: async (root, _args, context) => {
      // context.clients handles authentication automatically
      // No API keys are hardcoded or exposed
      const product = await context.clients.commerce.catalog.getProduct(
        root.productID
      )

      const installments = product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []

      return installments.map((inst: any) => ({
        count: inst.NumberOfInstallments,
        value: inst.Value,
        totalValue: inst.TotalValuePlusInterestRate,
        interestRate: inst.InterestRate,
      }))
    },
  },
}

export default installmentResolver

错误

// src/components/ProductInstallments.tsx
// CRITICAL SECURITY ISSUE: API keys exposed in client-side code
import React, { useEffect, useState } from 'react'

export default function ProductInstallments({ productId }: { productId: string }) {
  const [installments, setInstallments] = useState([])

  useEffect(() => {
    fetch(`https://mystore.vtexcommercestable.com.br/api/catalog/pvt/product/${productId}`, {
      headers: {
        // CRITICAL: These keys are now visible to EVERY visitor of your site.
        // Anyone can extract them from the browser's network tab.
        'X-VTEX-API-AppKey': 'vtexappkey-mystore-ABCDEF',
        'X-VTEX-API-AppToken': 'very-secret-token-12345',
      },
    })
      .then((res) => res.json())
      .then((data) => setInstallments(data.Installments))
  }, [productId])

  return <div>{installments.length} installments available</div>
}

限制:遵循 API 扩展目录结构

必须将 API 扩展文件放在正确的目录结构中:VTEX API 扩展放在 src/graphql/vtex/,第三方 API 扩展放在 src/graphql/thirdParty/。每个目录都必须包含 typeDefs/ 和 resolvers/ 子目录。

重要原因 FastStore 的构建系统会从这些特定目录中发现并编译 API 扩展。放在其他位置的文件不会被纳入 GraphQL schema,解析器也不会执行。构建时不会报错——扩展字段只会不存在,导致组件尝试查询这些字段时出现运行时 GraphQL 错误。

检测 如果在 src/graphql/vtex/ 或 src/graphql/thirdParty/ 之外看到 GraphQL 类型定义(.graphql 文件)或解析器文件 → 提醒这些文件不会被构建系统发现。如果缺少 typeDefs/ 或 resolvers/ 子目录 → 提醒目录结构不正确。

正确

# src/graphql/vtex/typeDefs/product.graphql
type StoreProduct {
  availableInstallments: [Installment]
}

type Installment {
  count: Int
  value: Float
  totalValue: Float
  interestRate: Float
}
// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'

const productResolver: Record<string, Resolver> = {
  StoreProduct: {
    availableInstallments: async (root, _args, context) => {
      const product = await context.clients.commerce.catalog.getProduct(
        root.productID
      )
      const installments =
        product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []

      return installments.map((inst: any) => ({
        count: inst.NumberOfInstallments,
        value: inst.Value,
        totalValue: inst.TotalValuePlusInterestRate,
        interestRate: inst.InterestRate,
      }))
    },
  },
}

export default productResolver
// src/graphql/vtex/resolvers/index.ts
import { default as StoreProductResolver } from './product'

const resolvers = {
  ...StoreProductResolver,
}

export default resolvers

错误

// WRONG: Resolver placed in src/api/ instead of src/graphql/vtex/resolvers/
// src/api/resolvers/product.ts
// This file will NOT be discovered by FastStore's build system.
// The GraphQL schema will NOT include the extended fields.
// Components querying these fields will get runtime errors.

const productResolver = {
  StoreProduct: {
    availableInstallments: async (root: any) => {
      return []
    },
  },
}

export default productResolver

首选模式

API 扩展的推荐文件布局:

src/
├── graphql/
│   ├── vtex/
│   │   ├── typeDefs/
│   │   │   └── product.graphql
│   │   └── resolvers/
│   │       ├── product.ts
│   │       └── index.ts
│   └── thirdParty/
│       ├── typeDefs/
│       │   └── extra.graphql
│       └── resolvers/
│           ├── reviews.ts
│           └── index.ts
└── fragments/
    ├── ServerProduct.ts    ← server-side fragment (SSR)
    └── ClientProduct.ts    ← client-side fragment (post-render)

最精简的 API 扩展——向 StoreProduct 添加字段:

# src/graphql/vtex/typeDefs/product.graphql
type StoreProduct {
  availableInstallments: [Installment]
}

type Installment {
  count: Int!
  value: Float!
  totalValue: Float!
  interestRate: Float!
}
// src/graphql/vtex/resolvers/product.ts
import type { Resolver } from '@faststore/api'

const productResolver: Record<string, Resolver> = {
  StoreProduct: {
    availableInstallments: async (root, _args, context) => {
      const product = await context.clients.commerce.catalog.getProduct(
        root.productID
      )
      const installments =
        product.items?.[0]?.sellers?.[0]?.commertialOffer?.Installments || []

      return installments.map((inst: any) => ({
        count: inst.NumberOfInstallments,
        value: inst.Value,
        totalValue: inst.TotalValuePlusInterestRate,
        interestRate: inst.InterestRate,
      }))
    },
  },
}

export default productResolver

通过片段在查询中包含新字段:

// src/fragments/ServerProduct.ts
import { gql } from '@faststore/core/api'

export const fragment = gql(`
  fragment ServerProduct on Query {
    product(locator: $locator) {
      availableInstallments {
        count
        value
        totalValue
        interestRate
      }
    }
  }
`)

第三方 API 扩展(例如,商品评论):

// src/graphql/thirdParty/resolvers/reviews.ts
import type { Resolver } from '@faststore/api'

const REVIEWS_API_KEY = process.env.REVIEWS_API_KEY // Server-only env var (no NEXT_PUBLIC_ prefix)

const reviewsResolver: Record<string, Resolver> = {
  StoreProduct: {
    reviews: async (root) => {
      const response = await fetch(
        `https://api.reviews-service.com/products/${root.productID}/reviews`,
        {
          headers: {
            Authorization: `Bearer ${REVIEWS_API_KEY}`,
          },
        }
      )
      const data = await response.json()
      return {
        averageRating: data.average_rating,
        totalReviews: data.total_count,
        reviews: data.reviews.slice(0, 5).map((r: any) => ({
          author: r.author_name,
          rating: r.rating,
          text: r.review_text,
          date: r.created_at,
        })),
      }
    },
  },
}

export default reviewsResolver

常见失败模式

  • 在 React 组件中直接调用 VTEX Catalog API 的 REST 接口——会造成 CORS 问题、绕过缓存,并向浏览器暴露 VTEX 账户结构。
  • 在客户端代码或 NEXT_PUBLIC_ 环境变量中暴露 API 密钥(VTEX_APP_KEY、VTEX_APP_TOKEN)——这是严重的安全漏洞。
  • 将解析器或类型定义放在 src/graphql/vtex/ 或 src/graphql/thirdParty/ 之外——构建系统不会发现这些文件。
  • 不使用 API 扩展系统,而是创建自定义 Next.js API 路由(pages/api/)——这会绕过缓存、类型安全和请求批处理。
  • 忘记创建重新导出所有解析器的解析器索引文件(src/graphql/vtex/resolvers/index.ts)。

审查清单

  • 所有目录数据是否都通过 FastStore GraphQL API 获取(而不是在组件中直接调用 REST 接口)?
  • API 扩展文件是否位于 src/graphql/vtex/ 或 src/graphql/thirdParty/ 中,并包含正确的 typeDefs/ 和 resolvers/ 子目录?
  • 解析器索引文件是否重新导出了所有解析器?
  • API 密钥和机密是否仅用于服务端解析器(没有 NEXT_PUBLIC_ 前缀)?
  • 是否在 src/fragments/ 中创建了片段,以便在预定义查询中包含新字段?
  • 是否不存在可由 API 扩展替代的自定义 Next.js API 路由?

参考资料

评论

支持 Markdown