vtex-io-service-apps

Compare original and translation side by side

🇺🇸

Original

English
🇨🇳

Translation

Chinese

Backend 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 (
    node/index.ts
    ) with typed context, clients, and state
  • Creating and registering custom clients extending JanusClient or ExternalClient
  • Using
    ctx.clients
    to access clients with built-in caching, retry, and metrics
  • Configuring routes and middleware chains in service.json
Do not use this skill for:
  • Manifest and builder configuration (use
    vtex-io-app-structure
    instead)
  • GraphQL schema definitions (use
    vtex-io-graphql-api
    instead)
  • React component development (use
    vtex-io-react-apps
    instead)
当你开发需要后端逻辑的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 (
    node/index.ts
    ) is the entry point for every VTEX IO backend app. It receives clients, routes (with middleware chains), GraphQL resolvers, and event handlers.
  • Every middleware, resolver, and event handler receives
    ctx
    with:
    ctx.clients
    (registered clients),
    ctx.state
    (mutable per-request state),
    ctx.vtex
    (auth tokens, account info),
    ctx.body
    (request/response body).
  • Use
    JanusClient
    for VTEX internal APIs (base URL:
    https://{account}.vtexcommercestable.com.br
    ).
  • Use
    ExternalClient
    for non-VTEX APIs (any URL you specify).
  • Use
    AppClient
    for routes exposed by other VTEX IO apps.
  • Use
    MasterDataClient
    for Master Data v2 CRUD operations.
  • Register custom clients by extending
    IOClients
    — each client is lazily instantiated on first access via
    this.getOrSet()
    .
  • Keep clients as thin data-access wrappers. Put business logic in middlewares or service functions.
Client hierarchy:
ClassUse CaseBase URL
JanusClient
Access VTEX internal APIs (Janus gateway)
https://{account}.vtexcommercestable.com.br
ExternalClient
Access external (non-VTEX) APIsAny URL you specify
AppClient
Access routes exposed by other VTEX IO apps
https://{workspace}--{account}.myvtex.com
InfraClient
Access VTEX IO infrastructure servicesInternal
MasterDataClient
Access Master Data v2 CRUD operationsVTEX 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类(
    node/index.ts
    )是每个VTEX IO后端应用的入口,它接收客户端、路由(含中间件链)、GraphQL解析器和事件处理器。
  • 每个中间件、解析器和事件处理器都会接收
    ctx
    对象,其中包含:
    ctx.clients
    (已注册的客户端)、
    ctx.state
    (每个请求的可变状态)、
    ctx.vtex
    (认证令牌、账户信息)、
    ctx.body
    (请求/响应体)。
  • 访问VTEX内部API时使用
    JanusClient
    (基础URL:
    https://{account}.vtexcommercestable.com.br
    )。
  • 访问非VTEX API时使用
    ExternalClient
    (可指定任意URL)。
  • 访问其他VTEX IO应用暴露的路由时使用
    AppClient
  • 进行Master Data v2的CRUD操作时使用
    MasterDataClient
  • 通过继承
    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
架构:
text
请求 → VTEX IO Runtime → Service
  ├── 路由 → 中间件链 → ctx.clients.{name}.method()
  ├── GraphQL → 解析器 → ctx.clients.{name}.method()
  └── 事件 → 处理器 → ctx.clients.{name}.method()
                         客户端(JanusClient / ExternalClient)
                    外部服务 / VTEX API

Hard 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
@vtex/api
clients (JanusClient, ExternalClient, AppClient, or native clients from
@vtex/clients
). You MUST NOT use
axios
,
fetch
,
got
,
node-fetch
, or any other raw HTTP library.
Why 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
@vtex/api
clients properly route through the infrastructure.
Detection
If you see
import axios from 'axios'
,
import fetch from 'node-fetch'
,
import got from 'got'
,
require('node-fetch')
, or any direct
fetch()
call in a VTEX IO service app, STOP. Replace with a proper client extending JanusClient or ExternalClient.
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通信必须通过
@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的合规客户端。
正确示例
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
ctx.clients.{clientName}
in middlewares, resolvers, and event handlers. You MUST NOT instantiate client classes directly with
new
.
Why 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
new MyClient(...)
or
new ExternalClient(...)
inside a middleware or resolver, STOP. The client should be registered in the Clients class and accessed via
ctx.clients
.
Correct
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(账户、工作区、认证令牌)正确初始化客户端。直接实例化的客户端将缺少认证上下文、缓存配置,且无法接入指标统计管道。
检测方式
如果在中间件或解析器中看到
new MyClient(...)
new ExternalClient(...)
,请立即停止。客户端应注册到Clients类中,并通过
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
service.json
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.
Correct
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平台专为小型、专注且可组合的应用设计。
检测方式
如果
service.json
定义了超过10个路由,建议开发者考虑将应用拆分为更小的服务。这是一个软限制——可能存在合理的例外情况。
正确示例
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
ctx.clients
and
ctx.state
:
typescript
// 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.clients
ctx.state
创建中间件:
typescript
// 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
    ExternalClient
    or
    JanusClient
    instead.
  • 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
    new MyClient(...)
    inside a middleware creates clients without auth context, caching, or metrics. Always access via
    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
    @vtex/api
    clients (no axios, fetch, got)?
  • Are all clients accessed via
    ctx.clients
    , never instantiated with
    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
    service.json
    have reasonable route count (≤10)?
  • Are client options (retries, timeout) configured appropriately?
  • 所有HTTP调用是否都通过
    @vtex/api
    客户端进行(无axios、fetch、got)?
  • 所有客户端是否都通过
    ctx.clients
    访问,从未使用
    new
    实例化?
  • 自定义客户端是否已注册到IOClients类中?
  • Service入口文件是否正确整合了客户端、路由、解析器和事件?
  • 业务逻辑是否放在中间件/解析器中,而非客户端类中?
  • service.json
    中的路由数量是否合理(≤10)?
  • 客户端选项(重试次数、超时时间)是否配置得当?

Reference

参考资料