vtex-io-service-apps
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseBackend Service Apps & API Clients
后端服务应用与API客户端
When this skill applies
适用场景
Use this skill when developing a VTEX IO app that needs backend logic — REST API routes, GraphQL resolvers, event handlers, scheduled tasks, or integrations with VTEX Commerce APIs and external services.
- Building the Service entry point () with typed context, clients, and state
node/index.ts - Creating and registering custom clients extending JanusClient or ExternalClient
- Using to access clients with built-in caching, retry, and metrics
ctx.clients - Configuring routes and middleware chains in service.json
Do not use this skill for:
- Manifest and builder configuration (use instead)
vtex-io-app-structure - GraphQL schema definitions (use instead)
vtex-io-graphql-api - React component development (use instead)
vtex-io-react-apps
当你开发需要后端逻辑的VTEX IO应用时,可使用本技能,包括REST API路由、GraphQL解析器、事件处理器、定时任务,或是与VTEX Commerce API及外部服务的集成。
- 构建带有类型化上下文、客户端和状态的Service入口文件()
node/index.ts - 创建并注册继承自JanusClient或ExternalClient的自定义客户端
- 使用访问具备内置缓存、重试和指标统计功能的客户端
ctx.clients - 在service.json中配置路由和中间件链
请勿将本技能用于以下场景:
- 清单和构建器配置(请使用)
vtex-io-app-structure - GraphQL schema定义(请使用)
vtex-io-graphql-api - React组件开发(请使用)
vtex-io-react-apps
Decision rules
决策规则
- The Service class () is the entry point for every VTEX IO backend app. It receives clients, routes (with middleware chains), GraphQL resolvers, and event handlers.
node/index.ts - Every middleware, resolver, and event handler receives with:
ctx(registered clients),ctx.clients(mutable per-request state),ctx.state(auth tokens, account info),ctx.vtex(request/response body).ctx.body - Use for VTEX internal APIs (base URL:
JanusClient).https://{account}.vtexcommercestable.com.br - Use for non-VTEX APIs (any URL you specify).
ExternalClient - Use for routes exposed by other VTEX IO apps.
AppClient - Use for Master Data v2 CRUD operations.
MasterDataClient - Register custom clients by extending — each client is lazily instantiated on first access via
IOClients.this.getOrSet() - Keep clients as thin data-access wrappers. Put business logic in middlewares or service functions.
Client hierarchy:
| Class | Use Case | Base URL |
|---|---|---|
| Access VTEX internal APIs (Janus gateway) | |
| Access external (non-VTEX) APIs | Any URL you specify |
| Access routes exposed by other VTEX IO apps | |
| Access VTEX IO infrastructure services | Internal |
| Access Master Data v2 CRUD operations | VTEX API |
Architecture:
text
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- Service类()是每个VTEX IO后端应用的入口,它接收客户端、路由(含中间件链)、GraphQL解析器和事件处理器。
node/index.ts - 每个中间件、解析器和事件处理器都会接收对象,其中包含:
ctx(已注册的客户端)、ctx.clients(每个请求的可变状态)、ctx.state(认证令牌、账户信息)、ctx.vtex(请求/响应体)。ctx.body - 访问VTEX内部API时使用(基础URL:
JanusClient)。https://{account}.vtexcommercestable.com.br - 访问非VTEX API时使用(可指定任意URL)。
ExternalClient - 访问其他VTEX IO应用暴露的路由时使用。
AppClient - 进行Master Data v2的CRUD操作时使用。
MasterDataClient - 通过继承注册自定义客户端——每个客户端在首次通过
IOClients访问时延迟实例化。this.getOrSet() - 客户端应保持为轻量的数据访问包装器。将业务逻辑放在中间件或服务函数中。
客户端层级:
| 类名 | 使用场景 | 基础URL |
|---|---|---|
| 访问VTEX内部API(Janus网关) | |
| 访问外部(非VTEX)API | 可指定任意URL |
| 访问其他VTEX IO应用暴露的路由 | |
| 访问VTEX IO基础设施服务 | 内部地址 |
| 进行Master Data v2的CRUD操作 | VTEX API |
架构:
text
请求 → VTEX IO Runtime → Service
├── 路由 → 中间件链 → ctx.clients.{name}.method()
├── GraphQL → 解析器 → ctx.clients.{name}.method()
└── 事件 → 处理器 → ctx.clients.{name}.method()
│
▼
客户端(JanusClient / ExternalClient)
│
▼
外部服务 / VTEX APIHard constraints
硬性约束
Constraint: Use @vtex/api Clients — Never Raw HTTP Libraries
约束:使用@vtex/api客户端——禁止使用原生HTTP库
All HTTP communication from a VTEX IO service app MUST go through clients (JanusClient, ExternalClient, AppClient, or native clients from ). You MUST NOT use , , , , or any other raw HTTP library.
@vtex/api@vtex/clientsaxiosfetchgotnode-fetchWhy this matters
VTEX IO clients provide automatic authentication header injection, built-in caching (disk and memory), retry with exponential backoff, timeout management, native metrics and billing tracking, and proper error handling. Raw HTTP libraries bypass all of these. Additionally, outbound traffic from VTEX IO is firewalled — only clients properly route through the infrastructure.
@vtex/apiDetection
If you see , , , , or any direct call in a VTEX IO service app, STOP. Replace with a proper client extending JanusClient or ExternalClient.
import axios from 'axios'import fetch from 'node-fetch'import got from 'got'require('node-fetch')fetch()Correct
typescript
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',
})
}
}Wrong
typescript
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
}VTEX IO服务应用的所有HTTP通信必须通过客户端(JanusClient、ExternalClient、AppClient或中的原生客户端)进行。严禁使用、、、或其他原生HTTP库。
@vtex/api@vtex/clientsaxiosfetchgotnode-fetch重要性说明
VTEX IO客户端提供自动认证头注入、内置缓存(磁盘和内存)、指数退避重试、超时管理、原生指标统计和计费跟踪,以及完善的错误处理机制。原生HTTP库会绕过所有这些功能。此外,VTEX IO的出站流量受防火墙限制——只有客户端能正确通过基础设施路由。
@vtex/api检测方式
如果在VTEX IO服务应用中看到、、、或直接调用,请立即停止。替换为继承JanusClient或ExternalClient的合规客户端。
import axios from 'axios'import fetch from 'node-fetch'import got from 'got'require('node-fetch')fetch()正确示例
typescript
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',
})
}
}错误示例
typescript
import axios from 'axios'
// 完全绕过VTEX IO基础设施
// 无缓存、无重试、无指标统计、无认证令牌注入
// 出站请求可能被防火墙拦截
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
}Constraint: Access Clients via ctx.clients — Never Instantiate Directly
约束:通过ctx.clients访问客户端——禁止直接实例化
Clients MUST always be accessed through in middlewares, resolvers, and event handlers. You MUST NOT instantiate client classes directly with .
ctx.clients.{clientName}newWhy this matters
The IOClients registry manages client lifecycle, ensuring proper initialization with the current request's IOContext (account, workspace, auth tokens). Direct instantiation creates clients without authentication context, without caching configuration, and without connection to the metrics pipeline.
Detection
If you see or inside a middleware or resolver, STOP. The client should be registered in the Clients class and accessed via .
new MyClient(...)new ExternalClient(...)ctx.clientsCorrect
typescript
// 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()
}Wrong
typescript
// 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()
}必须始终通过中间件、解析器和事件处理器中的访问客户端。严禁使用直接实例化客户端类。
ctx.clients.{clientName}new重要性说明
IOClients注册表管理客户端生命周期,确保使用当前请求的IOContext(账户、工作区、认证令牌)正确初始化客户端。直接实例化的客户端将缺少认证上下文、缓存配置,且无法接入指标统计管道。
检测方式
如果在中间件或解析器中看到或,请立即停止。客户端应注册到Clients类中,并通过访问。
new MyClient(...)new ExternalClient(...)ctx.clients正确示例
typescript
// 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()
}错误示例
typescript
// node/middlewares/getProduct.ts
import { CatalogClient } from '../clients/catalogClient'
export async function getProduct(ctx: Context, next: () => Promise<void>) {
// 直接实例化——无认证上下文、无缓存、无指标统计
const catalog = new CatalogClient(ctx.vtex, {})
const product = await catalog.getProductById(ctx.query.id)
ctx.body = product
ctx.status = 200
await next()
}Constraint: Avoid Monolithic Service Apps
约束:避免单体式服务应用
A single service app SHOULD NOT define more than 10 HTTP routes. If you need more, consider splitting into focused microservice apps.
Why this matters
VTEX IO apps run in containers with limited memory (max 512MB). A monolithic app with many routes increases memory usage, cold start time, and blast radius of failures. The VTEX IO platform is designed for small, focused apps that compose together.
Detection
If defines more than 10 routes, warn the developer to consider splitting the app into smaller services. This is a soft limit — there may be valid exceptions.
service.jsonCorrect
json
{
"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 }
}
}Wrong
json
{
"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 routes covering reviews, products, orders, users, categories, brands, and inventory — this should be 3-4 separate apps.
单个服务应用定义的HTTP路由不应超过10个。如果需要更多路由,考虑拆分为专注的微服务应用。
重要性说明
VTEX IO应用在内存受限的容器中运行(最大512MB)。包含大量路由的单体应用会增加内存占用、冷启动时间和故障影响范围。VTEX IO平台专为小型、专注且可组合的应用设计。
检测方式
如果定义了超过10个路由,建议开发者考虑将应用拆分为更小的服务。这是一个软限制——可能存在合理的例外情况。
service.json正确示例
json
{
"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 }
}
}错误示例
json
{
"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个独立应用。
Preferred pattern
推荐模式
Define custom clients:
typescript
// 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',
})
}
}Register clients in IOClients:
typescript
// 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)
}
}Create middlewares using and :
ctx.clientsctx.statetypescript
// 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()
}Wire everything in the Service entry point:
typescript
// 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,
},
},
})定义自定义客户端:
typescript
// 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中注册客户端:
typescript
// 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.clientsctx.statetypescript
// 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查询参数为必填项' }
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入口文件中整合所有内容:
typescript
// 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,
},
},
})Common failure modes
常见失败模式
- Using axios/fetch/got/node-fetch for HTTP calls: These libraries bypass the entire VTEX IO infrastructure — no automatic auth token injection, no caching, no retry logic, no metrics. Outbound requests may also be blocked by the firewall. Create a proper client extending or
ExternalClientinstead.JanusClient - Putting business logic in clients: Clients become bloated and hard to test. Keep clients as thin wrappers around HTTP calls. Put business logic in middlewares or dedicated service functions.
- Direct client instantiation: Using inside a middleware creates clients without auth context, caching, or metrics. Always access via
new MyClient(...).ctx.clients
- 使用axios/fetch/got/node-fetch进行HTTP调用:这些库绕过整个VTEX IO基础设施——无自动认证令牌注入、无缓存、无重试逻辑、无指标统计。出站请求也可能被防火墙拦截。应创建继承或
ExternalClient的合规客户端。JanusClient - 在客户端中放置业务逻辑:客户端会变得臃肿且难以测试。客户端应保持为HTTP调用的轻量包装器。将业务逻辑放在中间件或专用服务函数中。
- 直接实例化客户端:在中间件中使用创建的客户端缺少认证上下文、缓存和指标统计功能。始终通过
new MyClient(...)访问客户端。ctx.clients
Review checklist
审核清单
- Are all HTTP calls going through clients (no axios, fetch, got)?
@vtex/api - Are all clients accessed via , never instantiated with
ctx.clients?new - Are custom clients registered in the IOClients class?
- Does the Service entry point correctly wire clients, routes, resolvers, and events?
- Is business logic in middlewares/resolvers, not in client classes?
- Does have reasonable route count (≤10)?
service.json - Are client options (retries, timeout) configured appropriately?
- 所有HTTP调用是否都通过客户端进行(无axios、fetch、got)?
@vtex/api - 所有客户端是否都通过访问,从未使用
ctx.clients实例化?new - 自定义客户端是否已注册到IOClients类中?
- Service入口文件是否正确整合了客户端、路由、解析器和事件?
- 业务逻辑是否放在中间件/解析器中,而非客户端类中?
- 中的路由数量是否合理(≤10)?
service.json - 客户端选项(重试次数、超时时间)是否配置得当?
Reference
参考资料
- Services — Overview of VTEX IO backend service development
- Clients — Native client list and client architecture overview
- Developing Clients — Step-by-step guide for creating custom JanusClient and ExternalClient
- Using Node Clients — How to use @vtex/api and @vtex/clients in middlewares and resolvers
- Calling Commerce APIs — Tutorial for building a service app that calls VTEX Commerce APIs
- Best Practices for Avoiding Rate Limits — Why clients with caching prevent rate-limit issues
- Services — VTEX IO后端服务开发概述
- Clients — 原生客户端列表和客户端架构概述
- Developing Clients — 创建自定义JanusClient和ExternalClient的分步指南
- Using Node Clients — 如何在中间件和解析器中使用@vtex/api和@vtex/clients
- Calling Commerce APIs — 构建调用VTEX Commerce API的服务应用教程
- Best Practices for Avoiding Rate Limits — 带缓存的客户端如何避免限流问题的最佳实践