vtex-io-graphql-api
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseGraphQL Schemas & Resolvers
GraphQL Schemas & Resolvers
When this skill applies
本技能的适用场景
Use this skill when your VTEX IO app needs to expose a GraphQL API — either for frontend React components to query, for other VTEX IO apps to consume, or for implementing custom data aggregation layers over VTEX Commerce APIs.
- Defining schemas in files in the
.graphqldirectory/graphql - Writing resolver functions in TypeScript in
/node/resolvers/ - Configuring and
@cacheControldirectives@auth - Wiring resolvers into the Service class
Do not use this skill for:
- Backend service structure and client system (use instead)
vtex-io-service-apps - Manifest and builder configuration (use instead)
vtex-io-app-structure - MasterData integration details (use instead)
vtex-io-masterdata
当你的VTEX IO应用需要暴露GraphQL API时使用本技能——无论是供前端React组件查询、供其他VTEX IO应用调用,还是用于在VTEX Commerce API之上实现自定义数据聚合层。
- 在目录下的
/graphql文件中定义schema.graphql - 在目录下用TypeScript编写resolver函数
/node/resolvers/ - 配置和
@cacheControl指令@auth - 将resolver接入Service类
以下场景请勿使用本技能:
- 后端服务结构与客户端系统(请使用)
vtex-io-service-apps - 清单(Manifest)与构建器配置(请使用)
vtex-io-app-structure - MasterData集成细节(请使用)
vtex-io-masterdata
Decision rules
决策规则
- The builder processes
graphqlfiles in.graphqland merges them into a single schema./graphql - Split definitions across multiple files for maintainability: for root types,
schema.graphqlfor directive declarations,directives.graphqlfor custom types.types/*.graphql - Use on all public Query fields.
@cacheControl(scope: PUBLIC, maxAge: SHORT|MEDIUM|LONG)= shared CDN cache,PUBLIC= per-user cache.PRIVATE - Use on all Mutations and on Queries that return sensitive or user-specific data.
@auth - Never use on Mutations.
@cacheControl - Resolver function keys in the Service entry point MUST exactly match the field names in .
schema.graphql - Always use in resolvers for data access — never raw HTTP calls.
ctx.clients
Recommended directory structure:
text
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 organizationBuilt-in directives:
- :
@cacheControl(scope/PUBLIC),PRIVATE(maxAge30s,SHORT5min,MEDIUM1h)LONG - : Enforces valid VTEX authentication token. Without it, unauthenticated users can call the endpoint.
@auth - : Automatically caches query results in VTEX infrastructure.
@smartcache
- 构建器会处理
graphql目录下的/graphql文件,并将它们合并为一个单一的schema。.graphql - 为了可维护性,将定义拆分到多个文件中:用于根类型,
schema.graphql用于指令声明,directives.graphql用于自定义类型。types/*.graphql - 在所有公开的Query字段上使用。
@cacheControl(scope: PUBLIC, maxAge: SHORT|MEDIUM|LONG)表示共享CDN缓存,PUBLIC表示按用户缓存。PRIVATE - 在所有Mutation以及返回敏感或用户特定数据的Query上使用。
@auth - 切勿在Mutation上使用。
@cacheControl - Service入口点中的resolver函数键必须与中的字段名称完全匹配。
schema.graphql - 在resolver中始终使用进行数据访问——绝不使用原始HTTP调用。
ctx.clients
推荐的目录结构:
text
graphql/
├── schema.graphql # Query和Mutation根类型定义
├── directives.graphql # 自定义指令声明(@cacheControl、@auth)
└── types/
├── Review.graphql # 自定义类型定义
└── Product.graphql # 每个类型对应一个文件,便于组织内置指令:
- :包含
@cacheControl(scope/PUBLIC)、PRIVATE(maxAge30秒、SHORT5分钟、MEDIUM1小时)LONG - :强制要求有效的VTEX身份验证令牌。如果不使用该指令,未认证用户也可以调用端点。
@auth - :自动在VTEX基础设施中缓存查询结果。
@smartcache
Hard constraints
硬性约束
Constraint: Declare the graphql Builder
约束:声明graphql构建器
Any app using schema files MUST declare the builder in . The builder interprets the schema and registers it with the VTEX IO runtime.
.graphqlgraphqlmanifest.jsongraphqlWhy this matters
Without the builder declaration, the directory is completely ignored. Schema files will not be processed, resolvers will not be registered, and GraphQL queries will return "schema not found" errors. The app will link without errors but GraphQL will silently not work.
graphql/graphqlDetection
If you see files in a directory but the manifest does not include in , STOP and add the builder declaration.
.graphql/graphql"graphql": "1.x"buildersCorrect
json
{
"builders": {
"node": "7.x",
"graphql": "1.x"
}
}Wrong
json
{
"builders": {
"node": "7.x"
}
}Missing — the directory with schema files is ignored. GraphQL queries return errors because no schema is registered. The app links successfully, masking the problem.
"graphql": "1.x"/graphql任何使用schema文件的应用必须在中声明构建器。构建器会解析schema并将其注册到VTEX IO运行时。
.graphqlmanifest.jsongraphqlgraphql重要性
如果没有声明构建器,目录会被完全忽略。Schema文件不会被处理,resolver不会被注册,GraphQL查询会返回"schema not found"错误。应用可以正常链接,但GraphQL会静默失效。
graphql/graphql检测方式
如果在目录中存在文件,但清单中没有包含,请立即停止并添加该构建器声明。
/graphql.graphqlbuilders"graphql": "1.x"正确示例
json
{
"builders": {
"node": "7.x",
"graphql": "1.x"
}
}错误示例
json
{
"builders": {
"node": "7.x"
}
}缺少——包含schema文件的目录会被忽略。GraphQL查询会返回错误,因为没有注册schema。应用可以成功链接,从而掩盖问题。
"graphql": "1.x"/graphqlConstraint: Use @cacheControl on Public Queries
约束:在公开Query上使用@cacheControl
All public-facing Query fields (those fetching data that is not user-specific) MUST include the directive with an appropriate and . Mutations MUST NOT use .
@cacheControlscopemaxAge@cacheControlWhy this matters
Without , every query hits your resolver on every request — no CDN caching, no edge caching, no shared caching. This leads to unnecessary load on VTEX infrastructure, slow response times, and potential rate limiting. For public product data, caching is critical for performance.
@cacheControlDetection
If a Query field returns public data (not user-specific) and does not have , warn the developer to add it. If a Mutation has , STOP and remove it.
@cacheControl@cacheControlCorrect
graphql
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
}Wrong
graphql
type Query {
reviews(productId: String!, limit: Int): [Review]
myReviews: [Review]
}
type Mutation {
createReview(review: ReviewInput!): Review
@cacheControl(scope: PUBLIC, maxAge: LONG)
}No cache control on queries (every request hits the resolver), missing on user-specific data, and on a mutation (makes no sense).
@auth@cacheControl所有面向公开的Query字段(返回非用户特定数据的字段)必须包含指令,并指定合适的和。Mutation绝对不能使用。
@cacheControlscopemaxAge@cacheControl重要性
如果没有,每个查询的每次请求都会触发resolver——没有CDN缓存、边缘缓存或共享缓存。这会导致VTEX基础设施承受不必要的负载、响应时间缓慢,甚至可能触发速率限制。对于公开的产品数据,缓存对性能至关重要。
@cacheControl检测方式
如果某个Query字段返回公开数据(非用户特定)但没有,请提醒开发者添加该指令。如果Mutation使用了,请立即停止并移除它。
@cacheControl@cacheControl正确示例
graphql
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
}错误示例
graphql
type Query {
reviews(productId: String!, limit: Int): [Review]
myReviews: [Review]
}
type Mutation {
createReview(review: ReviewInput!): Review
@cacheControl(scope: PUBLIC, maxAge: LONG)
}查询没有缓存控制(每次请求都触发resolver),用户特定数据缺少,Mutation使用了(毫无意义)。
@auth@cacheControlConstraint: Resolver Names Must Match Schema Fields
约束:Resolver名称必须与Schema字段匹配
Resolver function keys in the Service entry point MUST exactly match the field names defined in . The resolver object structure must mirror the GraphQL type hierarchy.
schema.graphqlWhy this matters
The GraphQL runtime maps incoming queries to resolver functions by name. If the resolver key does not match the schema field name, the field will resolve to without any error — a silent failure that is extremely difficult to debug.
nullDetection
If a schema field has no matching resolver key (or vice versa), STOP. Cross-check every Query and Mutation field against the resolver registration in .
node/index.tsCorrect
graphql
type Query {
reviews(productId: String!): [Review]
reviewById(id: ID!): Review
}typescript
// node/index.ts — resolver keys match schema field names exactly
export default new Service({
graphql: {
resolvers: {
Query: {
reviews: reviewsResolver,
reviewById: reviewByIdResolver,
},
},
},
})Wrong
typescript
// 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"
},
},
},
})Both fields will silently resolve to null. No error in logs.
Service入口点中的resolver函数键必须与中定义的字段名称完全匹配。Resolver对象结构必须镜像GraphQL类型的层级。
schema.graphql重要性
GraphQL运行时通过名称将传入的查询映射到resolver函数。如果resolver键与schema字段名称不匹配,该字段会解析为且不会有任何错误——这是一种极难调试的静默失败。
null检测方式
如果某个schema字段没有对应的resolver键(反之亦然),请立即停止。仔细核对每个Query和Mutation字段与中的resolver注册。
node/index.ts正确示例
graphql
type Query {
reviews(productId: String!): [Review]
reviewById(id: ID!): Review
}typescript
// node/index.ts — resolver键与schema字段名称完全匹配
export default new Service({
graphql: {
resolvers: {
Query: {
reviews: reviewsResolver,
reviewById: reviewByIdResolver,
},
},
},
})错误示例
typescript
// node/index.ts — resolver键"getReviews"与schema字段"reviews"不匹配
export default new Service({
graphql: {
resolvers: {
Query: {
getReviews: reviewsResolver, // 错误!Schema中是"reviews",不是"getReviews"
getReviewById: reviewByIdResolver, // 错误!Schema中是"reviewById"
},
},
},
})这两个字段都会静默解析为null,日志中不会有错误。
Preferred pattern
推荐模式
Add the GraphQL builder to manifest:
json
{
"builders": {
"node": "7.x",
"graphql": "1.x"
}
}Define the schema:
graphql
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
}Define custom types:
graphql
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!
}Declare directives:
graphql
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_DEFINITIONImplement resolvers:
typescript
// 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
},
}Wire resolvers into the Service:
typescript
// 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,
},
},
})Testing the GraphQL API after linking:
graphql
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构建器:
json
{
"builders": {
"node": "7.x",
"graphql": "1.x"
}
}定义schema:
graphql
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
}定义自定义类型:
graphql
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!
}声明指令:
graphql
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实现resolvers:
typescript
// 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
},
}将resolver接入Service:
typescript
// 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:
graphql
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
}
}Common failure modes
常见失败模式
- Defining resolvers without matching schema fields: The GraphQL runtime only exposes fields defined in the schema. Resolvers without matching fields are silently ignored. Conversely, schema fields without resolvers return . Always define the schema first, then implement matching resolvers with identical names.
null - Querying external APIs directly in resolvers: Using or
fetch()bypasses theaxiosclient system, losing caching, retries, metrics, and authentication. Always use@vtex/apiin resolvers.ctx.clients - Missing @auth on mutation endpoints: Without , any anonymous user can call the mutation — a critical security vulnerability. Always add
@authto mutations and queries returning sensitive data.@auth - Missing @cacheControl on public queries: Every request hits the resolver without caching, causing unnecessary load and slow responses. Add appropriate cache directives to all public Query fields.
- 定义了resolver但没有匹配的schema字段:GraphQL运行时只暴露schema中定义的字段。没有匹配字段的resolver会被静默忽略。反之,没有resolver的schema字段会返回。请始终先定义schema,再实现名称完全匹配的resolver。
null - 在resolver中直接调用外部API:使用或
fetch()会绕过axios客户端系统,丢失缓存、重试、指标和身份验证功能。在resolver中始终使用@vtex/api。ctx.clients - Mutation端点缺少@auth:如果没有,任何匿名用户都可以调用该Mutation——这是严重的安全漏洞。请始终为Mutation和返回敏感数据的Query添加
@auth。@auth - 公开Query缺少@cacheControl:每次请求都会触发resolver,没有缓存会导致不必要的负载和缓慢的响应。为所有公开Query字段添加合适的缓存指令。
Review checklist
审核清单
- Is the builder declared in
graphql?manifest.json - Do all public Query fields have with appropriate scope and maxAge?
@cacheControl - Do all Mutations and sensitive Queries have ?
@auth - Do resolver function keys exactly match schema field names?
- Are resolvers using for data access (no raw HTTP calls)?
ctx.clients - Are directive declarations present in ?
directives.graphql - Is the resolver wired into the Service entry point under ?
graphql.resolvers
- 是否在中声明了
manifest.json构建器?graphql - 所有公开Query字段是否都带有,并指定了合适的scope和maxAge?
@cacheControl - 所有Mutation和敏感Query是否都带有?
@auth - Resolver函数键是否与schema字段名称完全匹配?
- Resolver是否使用进行数据访问(没有原始HTTP调用)?
ctx.clients - 指令声明是否存在于中?
directives.graphql - Resolver是否在Service入口点的下完成接入?
graphql.resolvers
Related skills
相关技能
- — Service app fundamentals needed for all GraphQL resolvers
vtex-io-service-apps - — Manifest and builder configuration that GraphQL depends on
vtex-io-app-structure - — MasterData integration commonly used as a data source in resolvers
vtex-io-masterdata
- — 所有GraphQL resolver都需要的Service应用基础知识
vtex-io-service-apps - — GraphQL依赖的清单与构建器配置
vtex-io-app-structure - — 常用于resolver数据源的MasterData集成
vtex-io-masterdata
Reference
参考资料
- GraphQL in VTEX IO — Overview of GraphQL usage in the VTEX IO platform
- GraphQL Builder — Builder reference for schema processing and directory structure
- Developing a GraphQL API in Service Apps — Step-by-step tutorial for building GraphQL APIs
- Integrating an App with a GraphQL API — How to consume GraphQL APIs from other VTEX IO apps
- GraphQL authorization in IO apps — How to implement and use the directive for protected GraphQL operations
@auth - Implementing cache in GraphQL APIs for IO apps — How to implement and use the directive for GraphQL operations
@cacheControl - Clients — How to use ctx.clients in resolvers for data access
- GraphQL in VTEX IO — VTEX IO平台中GraphQL使用的概述
- GraphQL Builder — 关于schema处理和目录结构的构建器参考
- Developing a GraphQL API in Service Apps — 构建GraphQL API的分步教程
- Integrating an App with a GraphQL API — 如何从其他VTEX IO应用调用GraphQL API
- GraphQL authorization in IO apps — 如何在IO应用中实现和使用指令来保护GraphQL操作
@auth - Implementing cache in GraphQL APIs for IO apps — 如何在IO应用的GraphQL API中实现和使用指令
@cacheControl - Clients — 如何在resolver中使用ctx.clients进行数据访问