关于 vtex-io-service-apps
面向在 VTEX IO 应用中实现后端逻辑的开发者技能,介绍服务入口、路由、中间件、事件处理器和注册式 API 客户端,并说明如何遵循 VTEX 基础设施规范。
主要功能
- 01用 node/index.ts 组织服务
- 02配置路由和中间件链
- 03创建并注册 API 客户端
- 04通过 ctx.clients 访问客户端
- 05处理解析器和事件
- 06检查 HTTP 调用与路由模式
适用场景
- 01为 VTEX IO 应用构建 REST API 路由
- 02连接 VTEX 或外部 API
- 03添加 GraphQL 解析器或事件处理器
- 04检查客户端注册和 HTTP 用法
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill vtex-io-service-apps安装后在 Claude Code 中输入/vtex-io-service-apps确认已加载。
详细介绍
后端服务应用与 API 客户端
适用场景
开发需要后端逻辑的 VTEX IO 应用时使用此技能,例如 REST API 路由、GraphQL 解析器、事件处理程序、计划任务,或与 VTEX Commerce API 和外部服务集成。
- 使用类型化的上下文、客户端和状态构建 Service 入口点(
node/index.ts) - 创建并注册扩展 JanusClient 或 ExternalClient 的自定义客户端
- 使用
ctx.clients访问具有内置缓存、重试和指标功能的客户端 - 在 service.json 中配置路由和中间件链
以下场景不要使用此技能:
- Manifest 和 builder 配置(改用
vtex-io-app-structure) - GraphQL 架构定义(改用
vtex-io-graphql-api) - React 组件开发(改用
vtex-io-react-apps)
决策规则
- Service 类(
node/index.ts)是每个 VTEX IO 后端应用的入口点。它接收客户端、路由(及其中间件链)、GraphQL 解析器和事件处理程序。 - 每个中间件、解析器和事件处理程序都会接收
ctx,其中包含:ctx.clients(已注册的客户端)、ctx.state(每个请求可变的状态)、ctx.vtex(身份验证令牌、账户信息)、ctx.body(请求/响应正文)。 - 使用
JanusClient访问 VTEX 内部 API(基础 URL:https://{account}.vtexcommercestable.com.br)。 - 使用
ExternalClient访问非 VTEX API(可指定任意 URL)。 - 使用
AppClient访问其他 VTEX IO 应用公开的路由。 - 使用
MasterDataClient执行 Master Data v2 CRUD 操作。 - 通过扩展
IOClients注册自定义客户端——每个客户端都会在首次访问时通过this.getOrSet()延迟实例化。 - 客户端应作为精简的数据访问封装层。将业务逻辑放在中间件或服务函数中。
客户端层级:
| 类 | 用途 | 基础 URL |
|---|---|---|
JanusClient | 访问 VTEX 内部 API(Janus 网关) | https://{account}.vtexcommercestable.com.br |
ExternalClient | 访问外部(非 VTEX)API | 可指定任意 URL |
AppClient | 访问其他 VTEX IO 应用公开的路由 | https://{workspace}--{account}.myvtex.com |
InfraClient | 访问 VTEX IO 基础设施服务 | 内部 |
MasterDataClient | 执行 Master Data v2 CRUD 操作 | VTEX API |
架构:
Request → VTEX IO Runtime → Service
├── routes → middleware chain → ctx.clients.{name}.method()
├── graphql → resolvers → ctx.clients.{name}.method()
└── events → handlers → ctx.clients.{name}.method()
│
▼
Client (JanusClient / ExternalClient)
│
▼
External Service / VTEX API
硬性约束
约束:使用 @vtex/api 客户端——绝不使用原始 HTTP 库
VTEX IO 服务应用中的所有 HTTP 通信都必须通过 @vtex/api 客户端(JanusClient、ExternalClient、AppClient 或来自 @vtex/clients 的原生客户端)进行。绝不能使用 axios、fetch、got、node-fetch 或任何其他原始 HTTP 库。
原因
VTEX IO 客户端提供自动注入身份验证标头、内置缓存(磁盘和内存)、指数退避重试、超时管理、原生指标与计费跟踪,以及妥善的错误处理。原始 HTTP 库会绕过所有这些功能。此外,VTEX IO 的出站流量受防火墙限制——只有 @vtex/api 客户端才能通过基础设施正确路由。
检测
如果在 VTEX IO 服务应用中看到 import axios from 'axios'、import fetch from 'node-fetch'、import got from 'got'、require('node-fetch') 或任何直接的 fetch() 调用,请立即停止。改用扩展 JanusClient 或 ExternalClient 的适当客户端。
正确
import type { InstanceOptions, IOContext } from '@vtex/api'
import { ExternalClient } from '@vtex/api'
export class WeatherClient extends ExternalClient {
constructor(context: IOContext, options?: InstanceOptions) {
super('https://api.weather.com', context, {
...options,
headers: {
'X-Api-Key': 'my-key',
...options?.headers,
},
})
}
public async getForecast(city: string): Promise<Forecast> {
return this.http.get(`/v1/forecast/${city}`, {
metric: 'weather-forecast',
})
}
}
错误
import axios from 'axios'
// This bypasses VTEX IO infrastructure entirely.
// No caching, no retries, no metrics, no auth token injection.
// Outbound requests may be blocked by the firewall.
export async function getForecast(city: string): Promise<Forecast> {
const response = await axios.get(`https://api.weather.com/v1/forecast/${city}`, {
headers: { 'X-Api-Key': 'my-key' },
})
return response.data
}
约束:通过 ctx.clients 访问客户端 — 切勿直接实例化
在中间件、解析器和事件处理程序中,必须始终通过 ctx.clients.{clientName} 访问客户端。切勿直接使用 new 实例化客户端类。
为何重要
IOClients 注册表负责管理客户端生命周期,确保客户端使用当前请求的 IOContext(账户、工作区、身份验证令牌)正确初始化。直接实例化会创建没有身份验证上下文、没有缓存配置,也没有连接到指标管道的客户端。
检测方法
如果在中间件或解析器中看到 new MyClient(...) 或 new ExternalClient(...),请立即停止。应在 Clients 类中注册客户端,并通过 ctx.clients 访问。
正确
// node/clients/index.ts
import { IOClients } from '@vtex/api'
import { CatalogClient } from './catalogClient'
export class Clients extends IOClients {
public get catalog() {
return this.getOrSet('catalog', CatalogClient)
}
}
// node/middlewares/getProduct.ts
export async function getProduct(ctx: Context, next: () => Promise<void>) {
const { clients: { catalog } } = ctx
const product = await catalog.getProductById(ctx.query.id)
ctx.body = product
ctx.status = 200
await next()
}
错误
// node/middlewares/getProduct.ts
import { CatalogClient } from '../clients/catalogClient'
export async function getProduct(ctx: Context, next: () => Promise<void>) {
// Direct instantiation — no auth context, no caching, no metrics
const catalog = new CatalogClient(ctx.vtex, {})
const product = await catalog.getProductById(ctx.query.id)
ctx.body = product
ctx.status = 200
await next()
}
约束:避免单体服务应用
单个服务应用不应定义超过 10 个 HTTP 路由。如果需要更多路由,请考虑拆分为职责明确的微服务应用。
为何重要
VTEX IO 应用运行在内存有限(最高 512MB)的容器中。包含大量路由的单体应用会增加内存使用量、冷启动时间和故障影响范围。VTEX IO 平台旨在让小型、职责明确的应用组合协作。
检测方法
如果 service.json 定义了超过 10 个路由,请提醒开发者考虑将应用拆分为更小的服务。这是宽松限制——某些情况下可能存在合理例外。
正确
{
"memory": 256,
"timeout": 30,
"routes": {
"get-reviews": { "path": "/_v/api/reviews", "public": false },
"get-review": { "path": "/_v/api/reviews/:id", "public": false },
"create-review": { "path": "/_v/api/reviews", "public": false },
"moderate-review": { "path": "/_v/api/reviews/:id/moderate", "public": false }
}
}
错误
{
"memory": 512,
"timeout": 60,
"routes": {
"route1": { "path": "/_v/api/reviews" },
"route2": { "path": "/_v/api/reviews/:id" },
"route3": { "path": "/_v/api/products" },
"route4": { "path": "/_v/api/products/:id" },
"route5": { "path": "/_v/api/orders" },
"route6": { "path": "/_v/api/orders/:id" },
"route7": { "path": "/_v/api/users" },
"route8": { "path": "/_v/api/users/:id" },
"route9": { "path": "/_v/api/categories" },
"route10": { "path": "/_v/api/categories/:id" },
"route11": { "path": "/_v/api/brands" },
"route12": { "path": "/_v/api/inventory" }
}
}
涵盖评价、商品、订单、用户、类别、品牌和库存的 12 个路由——这应该拆分为 3-4 个独立应用。
推荐模式
定义自定义客户端:
// node/clients/catalogClient.ts
import type { InstanceOptions, IOContext } from '@vtex/api'
import { JanusClient } from '@vtex/api'
export class CatalogClient extends JanusClient {
constructor(context: IOContext, options?: InstanceOptions) {
super(context, {
...options,
headers: {
VtexIdclientAutCookie: context.authToken,
...options?.headers,
},
})
}
public async getProduct(productId: string): Promise<Product> {
return this.http.get(`/api/catalog/pvt/product/${productId}`, {
metric: 'catalog-get-product',
})
}
public async listSkusByProduct(productId: string): Promise<Sku[]> {
return this.http.get(`/api/catalog_system/pvt/sku/stockkeepingunitByProductId/${productId}`, {
metric: 'catalog-list-skus',
})
}
}
在 IOClients 中注册客户端:
// node/clients/index.ts
import { IOClients } from '@vtex/api'
import { CatalogClient } from './catalogClient'
import { ReviewStorageClient } from './reviewStorageClient'
export class Clients extends IOClients {
public get catalog() {
return this.getOrSet('catalog', CatalogClient)
}
public get reviewStorage() {
return this.getOrSet('reviewStorage', ReviewStorageClient)
}
}
使用 ctx.clients 和 ctx.state 创建中间件:
// node/middlewares/getReviews.ts
import type { ServiceContext } from '@vtex/api'
import type { Clients } from '../clients'
type Context = ServiceContext<Clients>
export async function validateParams(ctx: Context, next: () => Promise<void>) {
const { productId } = ctx.query
if (!productId || typeof productId !== 'string') {
ctx.status = 400
ctx.body = { error: 'productId query parameter is required' }
return
}
ctx.state.productId = productId
await next()
}
export async function getReviews(ctx: Context, next: () => Promise<void>) {
const { productId } = ctx.state
const reviews = await ctx.clients.reviewStorage.getByProduct(productId)
ctx.status = 200
ctx.body = reviews
await next()
}
在 Service 入口点中连接所有组件:
// node/index.ts
import type { ParamsContext, RecorderState } from '@vtex/api'
import { Service, method } from '@vtex/api'
import { Clients } from './clients'
import { validateParams, getReviews } from './middlewares/getReviews'
import { createReview } from './middlewares/createReview'
import { resolvers } from './resolvers'
export default new Service<Clients, RecorderState, ParamsContext>({
clients: {
implementation: Clients,
options: {
default: {
retries: 2,
timeout: 5000,
},
catalog: {
retries: 3,
timeout: 10000,
},
},
},
routes: {
reviews: method({
GET: [validateParams, getReviews],
POST: [createReview],
}),
},
graphql: {
resolvers: {
Query: resolvers.queries,
Mutation: resolvers.mutations,
},
},
})
常见故障模式
- 使用 axios/fetch/got/node-fetch 进行 HTTP 调用:这些库会绕过整个 VTEX IO 基础设施——不会自动注入身份验证令牌、不会缓存、不会执行重试逻辑,也不会记录指标。出站请求还可能被防火墙阻止。请改为创建继承自
ExternalClient或JanusClient的合适客户端。 - 在客户端中编写业务逻辑:客户端会变得臃肿且难以测试。客户端应作为 HTTP 调用的轻量封装。业务逻辑应放在中间件或专用服务函数中。
- 直接实例化客户端:在中间件中使用
new MyClient(...)会创建没有身份验证上下文、缓存或指标的客户端。始终通过ctx.clients访问。
审查清单
- 所有 HTTP 调用是否都通过
@vtex/api客户端发出(没有使用 axios、fetch、got)? - 是否通过
ctx.clients访问所有客户端,而不是使用new实例化? - 自定义客户端是否已在 IOClients 类中注册?
- Service 入口点是否正确连接了客户端、路由、解析器和事件?
- 业务逻辑是否放在中间件/解析器中,而非客户端类中?
-
service.json中的路由数量是否合理(≤10)? - 客户端选项(重试、超时)是否配置得当?
参考资料
- 服务 — VTEX IO 后端服务开发概览
- 客户端 — 原生客户端列表和客户端架构概览
- 开发客户端 — 创建自定义 JanusClient 和 ExternalClient 的分步指南
- 使用 Node 客户端 — 如何在中间件和解析器中使用 @vtex/api 和 @vtex/clients
- 调用 Commerce API — 构建调用 VTEX Commerce API 的服务应用教程
- 避免触发速率限制的最佳实践 — 为什么带缓存的客户端可以避免速率限制问题