使用 itty-spec 构建类型安全 API:契约优先方法

发布: (2025年12月15日 GMT+8 05:43)
8 min read
原文: Dev.to

Source: Dev.to

Introduction

itty-spec 是一个强大的库,它为 itty‑router 带来了类型安全、合同优先的 API 开发方式。通过使用标准模式库来定义 API 合同,你可以获得:

  • 自动验证
  • 完整的 TypeScript 类型推断
  • 无缝的 OpenAPI 文档生成
  • 与边缘计算环境的兼容性

传统的 API 开发常常面临以下问题:

  • 手动验证散落在各个路由处理函数中
  • 类型定义与实际运行时行为脱节
  • 对无效请求的错误处理不一致
  • 文档过时,需要手动维护
  • 运行时无法保证处理函数与其文档化的合同匹配

这些问题会导致 bug、安全漏洞以及糟糕的开发者体验。itty-spec 通过将模式定义作为路由、验证、类型和文档的唯一真实来源来解决这些问题。

Contract‑First Approach

itty-spec 遵循合同优先的工作流:

  • 路由注册 – 自动根据合同定义创建路由
  • 请求验证 – 对所有进入的数据进行模式验证
  • 类型推断 – 为处理函数提供完整的 TypeScript 类型
  • 响应验证 – 确保处理函数返回的响应符合合同
  • 文档生成 – 自动生成 OpenAPI 规范

Typed Request Objects

当你定义合同后,TypeScript 会自动推断以下类型:

  • 路径参数 – 从类似 /users/:id 的模式中提取
  • 查询参数 – 类型化并验证的查询字符串
  • 请求头 – 已验证的头对象
  • 请求体 – 类型化的请求负载
import { createContract } from 'itty-spec';
import { z } from 'zod';

const contract = createContract({
  getUser: {
    path: '/users/:id',
    query: z.object({
      include: z.enum(['posts', 'comments']).optional(),
    }),
    headers: z.object({
      'x-api-key': z.string(),
    }),
    responses: {
      200: { body: z.object({ id: z.string(), name: z.string() }) },
    },
  },
});
// 在你的处理函数中,一切都是完整类型化的!
const router = createRouter({
  contract,
  handlers: {
    getUser: async (request) => {
      // request.params.id 被推断为 string
      // request.query.include 被推断为 'posts' | 'comments' | undefined
      // request.validatedHeaders['x-api-key'] 被推断为 string

      const userId = request.params.id; // string
      const include = request.query.include; // enum | undefined

      return request.json({ id: userId, name: 'John' });
    },
  },
});

类型系统可以防止意外访问不存在的属性,并为所有可用字段提供自动补全。

Validation Pipeline

itty-spec 使用中间件管道,在请求到达处理函数 之前 完成验证。验证顺序如下:

  1. 路径参数 – 提取并根据可选的 pathParams 模式进行验证
  2. 查询参数 – 解析并验证
  3. 请求头 – 规范化并验证
  4. 请求体 – 从 JSON 解析并验证(针对 POST/PUT/PATCH)

如果任何一步失败,请求会以 400 状态码被拒绝,并返回详细的错误信息,永远不会进入你的处理函数代码。

const contract = createContract({
  createUser: {
    path: '/users',
    method: 'POST',
    request: z.object({
      name: z.string().min(1).max(100),
      email: z.string().email(),
      age: z.number().int().min(18).max(120),
    }),
    responses: {
      200: { body: z.object({ id: z.string(), name: z.string() }) },
      400: { body: z.object({ error: z.string(), details: z.any() }) },
    },
  },
});

当请求体与模式不匹配时,验证会自动失败,使你的处理函数能够专注于业务逻辑。

Vendor‑Agnostic Schema Support

itty-spec 基于 Standard Schema V1 规范,允许你使用任何实现该规范的模式库:

  • Zod (v4) – 完全支持并可生成 OpenAPI
  • Valibot – 支持验证(计划支持 OpenAPI)
  • ArkType – 支持验证(计划支持 OpenAPI)
  • 任何其他实现 Standard Schema V1 的库

该规范定义了统一接口 (StandardSchemaV1) 包含:

  • ~standard.vendor – 标识使用的模式库
  • ~standard.validate() – 标准化的验证方法
  • 通过 TypeScript 实现的类型推断能力
// 与 Zod 配合使用
import { z } from 'zod';
const zodSchema = z.object({ name: z.string() });

// 与 Valibot 配合使用(待支持时)
import * as v from 'valibot';
const valibotSchema = v.object({ name: v.string() });

// 两者都可以在合同中使用
const contract = createContract({
  endpoint: {
    path: '/test',
    request: zodSchema, // 或者 valibotSchema
    responses: { 200: { body: zodSchema } },
  },
});

这种方式避免了对特定库的锁定,并简化了迁移过程。

Automatic OpenAPI 3.1 Generation

itty-spec 能够直接从合同生成完整的 OpenAPI 3.1 规范,省去手动维护文档的麻烦。

OpenAPI 生成器的关键特性

  • 从所有合同操作中提取模式
  • :param 路径语法转换为 {param}(OpenAPI 标准)
  • 将响应模式映射为 OpenAPI 响应对象
  • 包含头部、查询参数和请求体
  • 通过注册表系统对模式进行去重
import { createOpenApiSpecification } from 'itty-spec/openapi';
import { contract } from './contract';

const openApiSpec = createOpenApiSpecification(contract, {
  title: 'User Management API',
  version: '1.0.0',
  description: 'A comprehensive API for managing users',
  servers: [
    { url: 'https://api.example.com', description: 'Production' },
    { url: 'https://staging-api.example.com', description: 'Staging' },
  ],
});

生成的规范可用于:

  • 交互式文档(Swagger UI、Stoplight Elements)
  • 客户端 SDK 生成(openapi-generator、swagger-codegen)
  • API 测试工具(Postman、Insomnia)
  • CI/CD 验证

输出完全符合 OpenAPI 3.1,并可在任何支持的工具中使用。

Typed Response Helpers

itty-spec 在请求对象上添加了类型化的帮助方法,以确保响应符合合同定义:

  • request.json(body, status?, headers?) – 带类型验证的 JSON 响应
  • request.html(html, status?, headers?) – HTML 响应
  • request.error(status, body, headers?) – 错误响应
  • request.noContent(status) – 204 No Content 响应

这些帮助方法强制:

  • 只能使用合同中定义的有效状态码
  • 响应体必须匹配对应状态码的模式
  • 头部必须符合任何可选的头部模式
const contract = createContract({
  getUser: {
    path: '/users/:id',
    responses: {
      200: { body: z.object({ id: z.string(), name: z.string() }) },
      404: { body: z.object({ error: z.string() }) },
    },
  },
});

const router = createRouter({
  contract,
  handlers: {
    getUser: async (request) => {
      const user = await findUser(request.params.id);

      if (!user) {
        // TypeScript 确保这里匹配 404 响应模式
        return request.error(404, { error: 'User not found' });
      }

      return request.json({ id: user.id, name: user.name });
    },
  },
});

Conclusion

通过将模式设为唯一真实来源,itty-spec 带来了:

  • 全生命周期的类型安全
  • 自动验证,防止恶意或错误的输入
  • 零维护文档,通过 OpenAPI 自动生成
  • 灵活性,可选择任意兼容 Standard Schema V1 的库

采用 itty-spec 能简化 API 开发,降低 bug 率,并提升边缘环境下的开发者体验。

Back to Blog

相关文章

阅读更多 »

在 Cloudflare Workers 中触发长作业

问题:我的 Job 对 HTTP 来说太长 我有一个 Worker 负责处理我的 admin UI。 其中一个功能是一个按钮,用来启动一个繁重的 background process——...