关于 vtex-io-graphql-api
用于构建 VTEX IO GraphQL API 的开发技能,涵盖模式定义和 TypeScript 解析器。还介绍身份验证、缓存、解析器注册及通过客户端访问 VTEX 数据。
主要功能
- 01定义 GraphQL 模式
- 02编写 TypeScript 解析器
- 03配置身份验证和缓存指令
- 04将解析器绑定到模式字段
- 05使用 VTEX 客户端访问数据
适用场景
- 01向店铺前端组件提供数据
- 02为其他 VTEX IO 应用提供 API
- 03聚合 VTEX 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(SHORT30s、MEDIUM5min、LONG1h)@auth:强制使用有效的 VTEX 身份验证令牌。没有该指令时,未经身份验证的用户也能调用该端点。@smartcache:在 VTEX 基础设施中自动缓存查询结果。
硬性约束
约束:声明 graphql Builder
任何使用 .graphql 架构文件的应用都必须在 manifest.json 中声明 graphql builder。graphql builder 会解析架构,并将其注册到 VTEX IO 运行时中。
重要原因
如果未声明 graphql builder,/graphql 目录将被完全忽略。架构文件不会得到处理,解析器也不会注册,GraphQL 查询会返回“schema not found”错误。应用虽然可以成功 link,但 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 查询会返回错误。应用可以成功 link,从而掩盖问题。
约束:在公开查询上使用 @cacheControl
所有面向公众的 Query 字段(即获取非特定用户数据的字段)都必须包含 @cacheControl 指令,并设置合适的 scope 和 maxAge。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,且不会报错——这是一种极难调试的静默失败。
检测
如果架构字段没有匹配的解析器键(反之亦然),请停止。逐一核对每个 Query 和 Mutation 字段与 node/index.ts 中的解析器注册情况。
正确
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 builder 添加到清单中:
{
"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 字段添加适当的缓存指令。
审查清单
- 是否在
manifest.json中声明了graphqlbuilder? - 所有公开的 Query 字段是否都带有
@cacheControl,且 scope 和 maxAge 设置是否合适? - 所有 Mutation 和敏感 Query 是否都带有
@auth? - 解析器函数键是否与架构字段名称完全一致?
- 解析器是否使用
ctx.clients访问数据(没有直接调用 HTTP)? -
directives.graphql中是否存在指令声明? - 是否已在 Service 入口点的
graphql.resolvers下接入解析器?
相关技能
vtex-io-service-apps— 所有 GraphQL 解析器都需要了解的 Service 应用基础知识vtex-io-app-contract— GraphQL 所依赖的清单和 builder 配置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指令 - Clients — 如何在解析器中使用 ctx.clients 访问数据