关于 vtex-io-service-runtime
面向 VTEX IO 后端开发者的运行时技能,指导组织 node 服务入口、类型契约、路由与事件注册,以及 `service.json` 配置。适用于新建服务和排查注册或执行模型问题。
主要功能
- 01组织 node/index.ts 服务入口
- 02注册路由、事件和 GraphQL 处理器
- 03定义 Context、State 和参数类型
- 04在 service.json 配置运行参数
- 05明确路由的公开性
适用场景
- 01搭建新的 VTEX IO 后端应用
- 02审查服务入口中的职责划分
- 03配置超时、内存和副本
- 04排查路由或事件注册问题
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ vtex/ skills --skill vtex-io-service-runtime安装后在 Claude Code 中输入/vtex-io-service-runtime确认已加载。
详细介绍
服务运行时与执行模型
适用场景
当主要需要决定 VTEX IO 后端应用如何在 node builder 内运行时,使用此技能:包括如何组织 Service 入口点、如何声明运行时配置,以及如何将路由、事件或 GraphQL 处理程序注册到服务中。
- 在
node/下创建新的后端应用 - 将
node/index.ts组织为服务入口点 - 为处理程序定义类型化的
Context、State和 params 契约 - 配置
service.json中的超时、内存、工作进程和副本 - 排查由服务注册或执行模型不匹配导致的运行时问题
- 在运行时层注册 GraphQL 处理程序,同时将 schema 和 resolver 设计留在单独的技能中
以下情况不要使用此技能:
- 决定
manifest.json中的应用契约 - 设计自定义客户端或集成传输层
- 详细设计 HTTP 路由处理程序的行为
- 特定于事件的业务工作流
- 除运行时注册之外的 GraphQL schema 或 resolver 建模
决策规则
- 将
node/index.ts视为后端应用的运行时组合根。 - 使用
Service定义注册路由、事件和 GraphQL 处理程序等运行时入口,而不要直接在其中编写业务逻辑。 - 明确展示运行时接线:在服务边界清晰呈现上下文类型、客户端类型、路由注册和事件注册。
- 将超时、ttl、内存、工作进程和副本限制等执行参数放在
service.json中,而不是处理程序代码内。 - 使用
service.json声明平台执行服务时使用的运行时参数,尤其是memory、timeout、ttl、minReplicas、maxReplicas、workers、routes、events和rateLimitPerReplica。 - 使用
service.json中的routes暴露 HTTP 入口点。路由默认是私有的,因此必须显式设置public: true,才能使路由可从外部访问。 - 仅对幂等且可缓存、可安全地在重复请求间复用相同响应的路由使用
smartcache: true。避免在个性化、需认证或用于写入的端点上使用此设置。 - 使用
service.json中的events声明属于服务运行时的事件来源和处理程序。将事件注册保留在运行时层,将特定于事件的业务规则放在专门的事件模块中。 - 使用
rateLimitPerReplica控制每个副本的请求和事件吞吐量。仅在服务需要时设置全局基准值,然后仅针对开销较大的路由或噪声较大的事件源添加少量明确的覆盖值。 - 不要将
rateLimitPerReplica用作重新设计高开销路由、对工作进行排队或将耗时操作移至异步处理的替代方案。 - 让处理程序专注于请求或事件行为;让运行时结构专注于引导启动和注册。
- 谨慎设计
Context、State和 params 类型,使中间件与处理程序共享稳定的契约。对中间件应用相同的类型化Context和State,以便它们安全地操作ctx.state、ctx.vtex和 params,而不必退回到any。 - 如果后端应用开始在同一文件中混合运行时接线、客户端实现和业务规则,请先拆分这些职责,再继续扩展服务。
- 尽管
routes.access或routes.policies等某些授权字段可能位于service.json中,但它们主要属于授权范畴,应放在专注于认证或安全的技能中,而不是此运行时技能中。
运行时规格选择参考:
- 这些范围适用于合作伙伴和账户级应用。VTEX 原生核心服务可能会合理地使用高得多的数值,例如数千 MB 的内存或数百个副本,但不应将这些数值用作自定义应用的默认值。
建议默认值:
- 为同步 HTTP 服务设置 10 到 30 秒的
timeout。对于面向 UX 的路由,优先设置为 5 到 15 秒。 - 将
memory设为 256 MB。 - 将
workers设为 1。 - 对已安装的应用,默认使用
minReplicas: 2;仅在平台允许的关联应用开发环境中使用minReplicas: 1。 - 将
maxReplicas: 5作为最低的实用起始值,因为文档规定的最小值是5。 - 有意设置
ttl。在 VTEX IO 中,ttl以分钟为单位,其平台默认值和限制与timeout不同。对于合作伙伴应用,从默认的10分钟开始;只有在减少冷启动比让空闲实例更早休眠更重要时,才有意增加,最高可达60分钟。
扩缩范围和例外情况:
- 对于较简单的 IO 密集型服务,使用 128 到 256 MB;只有在出现 OOM、大型负载处理或较重型库等情况时,才增加到 512 MB。
- 只有在测量并确认收益后,才将
workers增至 2 到 4,以处理高吞吐量的 IO 密集型工作负载。默认情况下,每个实例使用的 worker 不要超过 4 个。 - 只有在公共流量或可预测的流量峰值有充分理由时,才将
maxReplicas从5增至10。对于合作伙伴应用,超过 10 的值属于例外情况,必须有明确的理由并进行监控。 - HTTP 路由的
timeout不要超过 60 秒;如果需要更长时间,应将流程重新设计为异步工作。 - 请记住,
ttl的文档规定最小值为10分钟,最大值为60分钟。对于低流量或突发流量的服务,应有意使用较高的值来减少冷启动;不要将ttl当作单个请求的超时时间。 - 对于合作伙伴应用,普通路由的
rateLimitPerReplica.perMinute通常从60到300起步,开销较大的路由则通常从10到60起步。rateLimitPerReplica.concurrent通常从1到5起步。
硬性约束
约束:Service 入口必须始终是运行时组合根
node/index.ts 必须定义并导出 VTEX IO 服务运行时结构,不能变成用于处理业务逻辑、数据转换或传输实现的万能文件。
为何这很重要
当入口文件将注册逻辑与业务逻辑混在一起时,执行模型会更难理解,处理程序会紧密耦合,对路由、事件或 GraphQL 接口的更改也会带来风险。
检测方法
如果 node/index.ts 包含大型处理程序代码、外部 API 调用、复杂分支或数据映射逻辑,请立即停止,并将这些逻辑移至专用模块。入口文件应专注于类型定义和注册。
正确
import type { ClientsConfig, RecorderState, ServiceContext } from '@vtex/api'
import { Service } from '@vtex/api'
import { clients, Clients } from './clients'
import { routes } from './routes'
export interface State extends RecorderState {}
export type Context = ServiceContext<Clients, State>
const clientsConfig: ClientsConfig<Clients> = {
implementation: clients,
options: {},
}
export default new Service<Clients, State>({
clients: clientsConfig,
routes,
})
错误
import { Service } from '@vtex/api'
import axios from 'axios'
export default new Service({
routes: {
reviews: async (ctx: any) => {
const response = await axios.get('https://example.com/data')
const transformed = response.data.items.map((item: any) => ({
...item,
extra: true,
}))
ctx.body = transformed.filter((item: any) => item.active)
},
},
})
约束:运行时配置必须写在 service.json 中,不得在代码中自行臆定
如果应用依赖超时、ttl、内存、工作进程或副本行为等资源和执行设置,则必须在 service.json 中进行配置。
service.json 位于 node/ 文件夹中,用于集中管理此服务的运行时参数,例如路由、事件、内存、超时、ttl、工作进程、副本和速率限制。
为何重要
这些设置属于服务与平台之间的运行时契约。将它们隐藏在假设中或分散在代码各处,会让行为更难预测,并可能导致超时、冷启动频繁、资源配置不足或扩缩容不匹配。在 VTEX IO 中,ttl 尤其重要,因为它以分钟为单位计量,并会影响服务基础设施在请求间隔期间进入空闲状态的积极程度。
对于低流量服务,使用最小 ttl 可能会增加冷启动,因为平台可以在突发流量之间更积极地缩减服务规模。
检测
如果应用依赖长时间运行的工作、并发、预热容量或特定的路由暴露行为,请停止并确认 service.json 中存在相关设置且设置有明确意图。如果相关行为只在代码注释或处理程序逻辑中有所暗示,请将其移入运行时配置。
正确
{
"memory": 256,
"timeout": 30,
"ttl": 10,
"minReplicas": 2,
"maxReplicas": 10,
"workers": 4,
"rateLimitPerReplica": {
"perMinute": 300,
"concurrent": 10
},
"routes": {
"reviews": {
"path": "/_v/api/reviews",
"public": false
}
}
}
错误
{
"routes": {
"reviews": {
"path": "/_v/api/reviews"
}
}
}
对于依赖明确的超时、并发、速率限制或副本行为的服务,此运行时配置并不完整,且未定义执行特性。
约束:必须在运行时契约中明确声明路由暴露方式
服务暴露的每个 HTTP 路由都必须在 service.json 中声明,并明确设置其可见性。若路由应为私有或公开,请勿依赖隐式默认值。
路由默认是私有的,因此当路由必须可从外部访问时,务必显式设置 public: true。
为何重要
路由可见性属于服务的运行时契约。如果暴露方式不明确,路由可能会以错误的可访问性发布,从而给私有处理程序带来安全风险,或导致预期公开的路由集成失败。
检测
如果服务运行时中存在路由,请停止并确认它已在 service.json 中声明,且 public 与预期的暴露方式一致。如果路由仅由可信的后台管理或应用间流程调用,请先确认其为私有,再考虑扩大访问范围。
正确
{
"routes": {
"status": {
"path": "/_v/status/health",
"public": true,
"smartcache": true
},
"reviews": {
"path": "/_v/api/reviews",
"public": false
}
}
}
错误
{
"routes": {
"reviews": {
"path": "/_v/api/reviews"
}
}
}
此路由未明确设置可见性,因此运行时契约无法清楚说明该端点是公开的还是受保护的。
约束:类型化上下文和状态必须与运行时注册的处理程序一致
服务必须定义与其注册的路由、事件或 GraphQL 处理程序相匹配的 Context、State 和处理程序契约。
为何重要
未类型化或不一致的运行时契约会使中间件组合变得脆弱,并允许处理程序依赖实际上无法保证存在的状态或参数。
检测
如果中间件或处理程序在没有共享类型化契约的情况下使用 ctx.state、ctx.clients、ctx.vtex 或参数字段,请停止并先引入或修正运行时类型,再添加更多处理程序。
正确
import type { ParamsContext, RecorderState, ServiceContext } from '@vtex/api'
interface State extends RecorderState {
reviewId?: string
}
type CustomContext = ServiceContext<Clients, State, ParamsContext>
export async function getReview(ctx: CustomContext) {
ctx.state.reviewId = ctx.vtex.route.params.id
ctx.body = { id: ctx.state.reviewId }
}
错误
export async function getReview(ctx: any) {
ctx.state.reviewId = ctx.params.review
ctx.body = { id: ctx.state.missingField.value }
}
首选模式
推荐的文件布局:
node/
├── index.ts
├── clients/
│ └── index.ts
├── routes/
│ └── index.ts
├── events/
│ └── index.ts
├── graphql/
│ └── index.ts
└── middlewares/
└── validate.ts
最简服务运行时模式:
import type { ClientsConfig, RecorderState, ServiceContext } from '@vtex/api'
import { Service } from '@vtex/api'
import { clients, Clients } from './clients'
import { routes } from './routes'
export interface State extends RecorderState {}
export type Context = ServiceContext<Clients, State>
const clientsConfig: ClientsConfig<Clients> = {
implementation: clients,
options: {},
}
export default new Service<Clients, State>({
clients: clientsConfig,
routes,
})
最简 service.json 模式:
{
"memory": 256,
"timeout": 30,
"ttl": 10,
"minReplicas": 2,
"maxReplicas": 5,
"workers": 1,
"rateLimitPerReplica": {
"perMinute": 120,
"concurrent": 4
},
"routes": {
"status": {
"path": "/_v/status/health",
"public": true,
"smartcache": true
},
"reviews": {
"path": "/_v/api/reviews",
"public": false
}
},
"events": {
"orderCreated": {
"sender": "vtex.orders-broadcast",
"topics": ["order-created"],
"rateLimitPerReplica": {
"perMinute": 60,
"concurrent": 2
}
}
}
}
使用服务入口点组合运行时接口,然后将业务逻辑放入处理程序、客户端和其他职责单一的模块中。
如果 routes/index.ts 或 events/index.ts 变得过大,请按领域拆分,例如 routes/orders.ts 或 events/catalog.ts,并让索引文件保持为精简的注册表。
常见失败模式
- 将业务逻辑直接放入
node/index.ts。 - 当运行时行为依赖明确的资源设置时,仍将
service.json视为可选项。 - 将
ttl设置得过低,导致服务在流量突发的间隔期过于频繁地休眠。 - 在个性化或面向写入的路由上启用
smartcache,可能导致不同请求之间错误地复用缓存。 - 注册路由、事件或 GraphQL 处理程序时,没有明确的类型化
Context和State。 - 将运行时组合与客户端实现细节混在一起。
- 让单个服务入口点在 HTTP、事件和 GraphQL 方面承担互不相关的职责,却没有清晰的模块边界。
审查清单
-
node/index.ts是否充当运行时组合根,而非业务逻辑文件? - 是否明确、清晰地注册了路由、事件和 GraphQL 处理程序?
-
service.json是否描述了应用实际依赖的运行时行为? - 处理程序是否一致地共享
Context、State和 params 类型? - 运行时关注点是否与客户端实现和业务逻辑分离?
参考资料
- 服务 - VTEX IO 服务运行时结构和注册方式
- Service JSON - VTEX IO 服务的运行时配置
- Node Builder -
node构建器下的后端应用结构 - 开发应用 - 后端应用开发的一般流程