vtex-io-client-integration
Client Integration & Service Access
When this skill applies
Use this skill when the main decision is how a VTEX IO backend app should call VTEX services or external APIs through the VTEX IO client system.
- Creating custom clients under
node/clients/ - Choosing between native clients from
@vtex/apior@vtex/clientsand a custom client - Registering clients in
IOClientsand exposing them throughctx.clients - Configuring
InstanceOptionssuch as retries, timeout, headers, or caching - Reviewing backend integrations that currently use raw HTTP libraries
Do not use this skill for:
- deciding the app contract in
manifest.json - structuring
node/index.tsor tuningservice.json - designing GraphQL schema or resolver contracts
- modeling route authorization or security permissions
- building storefront or admin frontend integrations
Decision rules
- Prefer native clients from
@vtex/apior@vtex/clientswhen they already cover the target VTEX service. Common examples include clients for catalog, checkout, logistics, and OMS. Write a custom client only when no suitable native client or factory exists. - Use
ExternalClientprimarily for non-VTEX external APIs. Avoid using it for VTEX-hosted endpoints such as*.myvtex.comor*.vtexcommercestable.com.brwhen a native client in@vtex/clients,JanusClient, or another documented higher-level VTEX client is available or more appropriate. - Janus is VTEX's Core Commerce API gateway. Use
JanusClientonly when you need to call a VTEX Core Commerce API through Janus and no suitable native client from@vtex/clientsalready exists. - Use
InfraClientonly for advanced integrations with VTEX IO infrastructure services under explicit documented guidance. In partner apps, prefer higher-level clients and factories such asmasterDataorvbaseinstead of extendingInfraClientdirectly. - Register every custom or native client in
node/clients/index.tsthrough aClientsclass that extendsIOClients. - Consume integrations through
ctx.clients, never by instantiating client classes inside middlewares, resolvers, or event handlers. - Keep clients focused on transport, request options, endpoint paths, and small response shaping. Keep business rules, authorization decisions, and orchestration outside the client.
- When building custom clients, always rely on the
IOContextpassed by VTEX IO such asaccount,workspace, and available auth tokens instead of hardcoding account names, workspaces, or environment-specific VTEX URLs. - Configure shared
InstanceOptionsin the runtime client config, then use client-specific overrides only when an integration has clearly different needs. - Use the
metricoption on important client calls so integrations can be tracked and monitored at the client layer, not only at the handler layer. - Keep error normalization close to the client boundary, but avoid hiding relevant HTTP status codes or transport failures that are important for observability and debugging.
- When integrating with external services, confirm that the required outbound policies are declared in the app contract, but keep the detailed policy modeling in auth or app-contract skills.
- In rare migration or legacy scenarios,
ExternalClientmay temporarily be used against VTEX-hosted endpoints, but treat this as an exception. The long-term goal should be to move toward native clients or the proper documented VTEX client abstractions so routing, authentication, and observability stay consistent.
Client selection guide:
| Client type | Use when | Avoid when |
|---|---|---|
ExternalClient |
calling non-VTEX external APIs | VTEX-hosted APIs that already have a native client or Janus-based abstraction |
JanusClient |
calling VTEX Core Commerce APIs not yet wrapped by @vtex/clients |
any VTEX service that already has a native client such as Catalog, Checkout, Logistics, or OMS |
InfraClient |
implementing advanced infra-style clients only under explicit documented guidance | general VTEX or external APIs in partner apps |
InstanceOptions heuristics:
- Start with small, explicit client defaults such as
retries: 2and a requesttimeoutbetween1000and3000milliseconds. - Use small finite retry values such as
1to3for idempotent operations. - Avoid automatic retries on non-idempotent operations unless the upstream API explicitly documents safe idempotency behavior.
- Do not use high retry counts to hide upstream instability. Surface repeated failures clearly and handle them intentionally in the business layer.
- Prefer per-client headers and metrics instead of scattering header definitions through handlers.
- Use memory or disk cache options only when repeated reads justify it and the response can be safely reused.
- Keep auth setup inside the client constructor or factory configuration, not duplicated across handlers.
Hard constraints
Constraint: All service-to-service HTTP calls must go through VTEX IO clients
HTTP communication from a VTEX IO backend app MUST go through @vtex/api or @vtex/clients clients. Do not use raw libraries such as axios, fetch, got, or node-fetch for service integrations.
Why this matters
VTEX IO clients provide transport behavior that raw libraries bypass, including authentication context, retries, metrics, caching options, and infrastructure-aware request execution. Raw HTTP calls make integrations harder to observe and easier to misconfigure.
Detection
If you see axios, fetch, got, node-fetch, or direct ad hoc HTTP code in a VTEX IO backend service, STOP and replace it with an appropriate VTEX IO client pattern.
Correct
import type { IOContext, InstanceOptions } 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-VTEX-Account': context.account,
'X-VTEX-Workspace': context.workspace,
'X-Api-Key': process.env.WEATHER_API_KEY,
...options?.headers,
},
})
}
public getForecast(city: string) {
return this.http.get(`/v1/forecast/${city}`, {
metric: 'weather-forecast',
})
}
}
Wrong
import axios from 'axios'
export async function getForecast(city: string) {
const response = await axios.get(`https://api.weather.com/v1/forecast/${city}`, {
headers: {
'X-Api-Key': process.env.WEATHER_API_KEY,
},
})
return response.data
}
Constraint: Clients must be registered in IOClients and consumed through ctx.clients
Clients MUST be registered in the Clients class that extends IOClients, and middlewares, resolvers, or event handlers MUST access them through ctx.clients.
Why this matters
The VTEX IO client registry ensures the current request context, options, caching behavior, and instrumentation are applied consistently. Direct instantiation inside handlers bypasses that shared lifecycle and creates fragile integration code.
Detection
If you see new MyClient(...) inside a middleware, resolver, or event handler, STOP. Move the client into node/clients/, register it in IOClients, and consume it through ctx.clients.
Correct
import { IOClients } from '@vtex/api'
import { Catalog } from '@vtex/clients'
export class Clients extends IOClients {
public get catalog() {
return this.getOrSet('catalog', Catalog)
}
}
export async function getSku(ctx: Context) {
const sku = await ctx.clients.catalog.getSkuById(ctx.vtex.route.params.id)
ctx.body = sku
}
Wrong
import { Catalog } from '@vtex/clients'
export async function getSku(ctx: Context) {
const catalog = new Catalog(ctx.vtex, {})
const sku = await catalog.getSkuById(ctx.vtex.route.params.id)
ctx.body = sku
}
Constraint: Choose the narrowest client type that matches the integration boundary
Each integration MUST use the correct client abstraction for its boundary. Do not default every integration to ExternalClient or JanusClient when a more specific client type or native package already exists.
Why this matters
The client type communicates intent and shapes how authentication, URLs, and service boundaries are handled. Using the wrong abstraction makes the integration harder to understand and more likely to drift from VTEX IO conventions.
Detection
If the target is a VTEX Core Commerce API, STOP and check whether a native client from @vtex/clients or JanusClient is more appropriate than ExternalClient. If the target is VTEX-hosted, STOP and confirm that there is no more specific documented VTEX client abstraction before defaulting to ExternalClient.
Correct
import type { IOContext, InstanceOptions } from '@vtex/api'
import { JanusClient } from '@vtex/api'
export class RatesAndBenefitsClient extends JanusClient {
constructor(context: IOContext, options?: InstanceOptions) {
super(context, options)
}
}
Wrong
import type { IOContext, InstanceOptions } from '@vtex/api'
import { ExternalClient } from '@vtex/api'
export class RatesAndBenefitsClient extends ExternalClient {
constructor(context: IOContext, options?: InstanceOptions) {
super(`https://${context.account}.vtexcommercestable.com.br`, context, options)
}
}
Preferred pattern
Recommended file layout:
node/
├── clients/
│ ├── index.ts
│ ├── catalog.ts
│ └── partnerApi.ts
├── middlewares/
│ └── getData.ts
└── index.ts
Register native and custom clients in one place:
import { IOClients } from '@vtex/api'
import { Catalog } from '@vtex/clients'
import { PartnerApiClient } from './partnerApi'
export class Clients extends IOClients {
public get catalog() {
return this.getOrSet('catalog', Catalog)
}
public get partnerApi() {
return this.getOrSet('partnerApi', PartnerApiClient)
}
}
Create custom clients with explicit routes and options:
import type { IOContext, InstanceOptions } from '@vtex/api'
import { ExternalClient } from '@vtex/api'
export class PartnerApiClient extends ExternalClient {
private routes = {
order: (id: string) => `/orders/${id}`,
}
constructor(context: IOContext, options?: InstanceOptions) {
super('https://partner.example.com', context, {
...options,
retries: 2,
timeout: 2000,
headers: {
'X-VTEX-Account': context.account,
'X-VTEX-Workspace': context.workspace,
...options?.headers,
},
})
}
public getOrder(id: string) {
return this.http.get(this.routes.order(id), {
metric: 'partner-get-order',
})
}
}
Wire shared client options in the runtime:
import type { ClientsConfig } from '@vtex/api'
import { Clients } from './clients'
const clients: ClientsConfig<Clients> = {
implementation: Clients,
options: {
default: {
retries: 2,
timeout: 2000,
},
},
}
Use clients from handlers through ctx.clients:
export async function getOrder(ctx: Context) {
const order = await ctx.clients.partnerApi.getOrder(ctx.vtex.route.params.id)
ctx.body = order
}
If a client file grows too large, split it by bounded integration domains and keep node/clients/index.ts as a small registry.
Common failure modes
- Using
axios,fetch, or other raw HTTP libraries in backend handlers instead of VTEX IO clients. - Instantiating clients directly inside handlers instead of registering them in
IOClients. - Choosing
ExternalClientwhen a native VTEX client or a more specific app client already exists. - Putting business rules, validation, or orchestration into clients instead of keeping them as transport wrappers.
- Scattering headers, auth setup, and retry settings across handlers instead of centralizing them in the client or shared client config.
- Forgetting the outbound-access policy required for an external integration declared in a custom client.
Review checklist
- Does each integration use the correct VTEX IO client abstraction?
- Are native clients from
@vtex/apior@vtex/clientspreferred when available? - Are clients registered in
IOClientsand consumed throughctx.clients? - Are raw HTTP libraries absent from the backend integration code?
- Are retries, timeouts, headers, and metrics configured in the client layer rather than scattered across handlers?
- Are business rules kept out of the client layer?
Reference
- Using Node Clients - How to consume clients through
ctx.clients - Developing Clients - How to build custom clients with
@vtex/api - Using VTEX IO clients - How to use VTEX clients for Core Commerce APIs
- Clients - VTEX IO client architecture and native client catalog