Agent X-Ray
RuntimeNotesAbout
Notes/代码工程/Encore/第4章

第4章:请求校验 —— 类型即校验与参数来源

4 分钟 · 更新于 2026-09-02

在 Express 里,TypeScript 类型是给编译器看的注释,运行时什么也不保证;在 Encore.ts 里,接口类型本身就是运行时校验 schema,且在 Rust 层执行。本章讲清"类型即校验"的完整规则与四种参数来源。


一、双重保障:编译时 + 运行时

typescript
import { api } from "encore.dev/api";

interface SearchRequest {
  depCity: string;
  arrCity: string;
  date: string;
}

export const search = api(
  { method: "POST", path: "/search", expose: true },
  async (req: SearchRequest): Promise<SearchResponse> => {
    // 进入这里时,req 三个字段一定存在且是 string —— 不需要手写判空
  },
);
  • 编译时:调用方(服务间调用、生成的客户端)传错类型直接编译失败;
  • 运行时:外部 HTTP 请求由 Rust 层按接口 schema 校验,缺字段/类型不符返回 400请求不会进入 JavaScript 处理函数

这带来两个工程含义:处理函数里不再需要防御性判空样板;恶意构造的畸形请求在 Rust 层就被挡掉,不消耗 JS 事件循环(顺带缓解 DoS)。

响应方向的校验只在编译时进行(你自己的代码返回什么类型编译器管得住,无需运行时再验)。

二、支持的类型

基本类型与结构

typescript
interface Request {
  name: string;               // 字符串
  age: number;                // 数字(int/float)
  vip: boolean;               // 布尔
  tags: string[];             // 数组
  passengers: { name: string; idNo: string }[];   // 对象数组
  mixed: (string | number)[]; // 多类型数组
  cabin: "economy" | "business" | "first";        // 字符串字面量枚举
}

修饰符

typescript
interface Request {
  remark?: string;          // 可选:缺失时为 undefined
  couponId: string | null;  // 可空:必须传,但可以是 null
}

?(可选)与 | null(可空)语义不同:可选是"可以不传",可空是"必须传但值可为 null"。两者可组合。

三、校验器:叠加约束

encore.dev/validate 导入校验器,用交叉类型 & 叠在字段上:

typescript
import { api } from "encore.dev/api";
import {
  Min, Max, MinLen, MaxLen,
  IsEmail, IsURL, StartsWith, EndsWith, MatchesRegexp,
} from "encore.dev/validate";

interface CreatePassengerRequest {
  email: string & IsEmail;                       // 合法邮箱
  name: string & MinLen<1> & MaxLen<64>;         // 1–64 字符
  age: number & Min<0> & Max<120>;               // 0–120
  homepage?: string & IsURL;                     // 可选;若传必须是 URL
  ticketNo: string & MatchesRegexp<"^[0-9]{3}-[0-9]{10}$">;  // 电子票号格式
  orderId: string & StartsWith<"ORD_">;          // 前缀约束
}

校验器全表

校验器适用类型示例
Min<N> / Max<N>numberprice: number & Min<0>
MinLen<N> / MaxLen<N>string、arraysegments: Segment[] & MaxLen<4>
IsEmailstringcontact: string & IsEmail
IsURLstringcallback: string & IsURL
StartsWith<S> / EndsWith<S>stringfile: string & EndsWith<".pdf">
MatchesRegexp<R>stringiata: string & MatchesRegexp<"^[A-Z]{3}$">

组合逻辑:&|

typescript
// 且:全部通过
count: number & (Min<3> & Max<1000>);

// 或:至少一个通过
contact: string & (IsURL | IsEmail);

// 混合:5–100 字符 且 是 URL
website: string & MinLen<5> & MaxLen<100> & IsURL;

校验失败统一返回 400:

json
{
  "code": "invalid_argument",
  "message": "validation failed",
  "details": { "field": "email", "error": "must be a valid email" }
}

四、四种参数来源

同一个请求接口里,不同字段可以来自 HTTP 请求的不同位置,用包装类型显式声明:

typescript
import { api, Query, Header, Cookie } from "encore.dev/api";

interface ListOrdersRequest {
  // 1. 路径参数:由 path 里的 :status 自动映射(同名字段)
  status: string;

  // 2. 查询串:?limit=20&offset=0
  limit?: Query<number>;
  offset?: Query<number>;

  // 3. 请求头
  clientVersion: Header<"X-Client-Version">;

  // 4. Cookie
  session?: Cookie<"session">;

  // 5. 其余字段 → 请求体(有 body 的方法)
  filterRemark?: string;
}

export const listOrders = api(
  { method: "POST", path: "/orders/:status", expose: true },
  async (req: ListOrdersRequest): Promise<ListOrdersResponse> => { /* ... */ },
);

默认归属规则

方法未加包装类型的字段默认去向
POST / PUT / PATCH请求体(JSON)
GET / HEAD / DELETE查询串

要点:

  • 路径参数不需要包装类型——path 里的 :name 段自动映射到接口同名字段;
  • Query<T> 在有 body 的方法里把字段显式移到查询串;GET 类方法里普通字段本来就走查询串,Query<> 可省;
  • Header<"名字"> 的泛型参数是真实的 HTTP 头名,字段名本身随意;
  • 嵌套对象内部的 Query / Header 无效——包装类型只在顶层字段生效,嵌套里会被当普通 body 字段。

响应方向

响应接口里的 Header<> / Cookie<> 同样有效:字段值会写进响应头/Set-Cookie,其余字段序列化为 JSON body。设置登录 Cookie 就是"在响应接口里放一个 Cookie<"session"> 字段"。

五、和 Zod 系方案的对比

维度Encore.tsExpress + Zod
schema 定义TypeScript 接口本身另写一份 Zod schema(与类型二选一或用 infer 派生)
执行位置Rust 层,进 JS 前JS 层,已进事件循环
文档同步自动(同一来源)需要 zod-to-openapi 等桥接
客户端类型自动生成手动导出/维护
校验表达力常用约束(表见上)任意自定义逻辑

Encore 覆盖了 90% 的常规校验场景;真正复杂的业务校验(如"往返日期不得早于去程")仍写在处理函数里,用 APIError.invalidArgument 抛出——结构校验交给框架,业务校验留在代码,边界清晰。

六、常见误区

  1. 以为 TypeScript 类型运行时会被擦除所以"还得自己验"——Encore 恰恰把接口编译成了 Rust 层校验器,这是它和裸 TS 最大的区别。
  2. 在嵌套对象里用 Query<> / Header<>——只在顶层生效,嵌套里静默退化为 body 字段。
  3. 分不清 ?| null——可选是可缺席,可空是必须到场但可为 null。
  4. 用正则校验器写超复杂业务规则——MatchesRegexp 适合格式校验(IATA 码、票号),跨字段业务规则进处理函数。
  5. GET 接口给字段包 Query<> 以为必须——GET 的普通字段默认就走查询串。
  6. 忘记校验失败根本不会进处理函数——在 handler 里打日志排查"为什么没收到请求"是徒劳,去看 400 响应体。

七、实践练习

  1. 为"航班搜索"接口建模:depCity/arrCityMatchesRegexp<"^[A-Z]{3}$"> 约束 IATA 三字码,date 约束 ^\d{4}-\d{2}-\d{2}$cabin 用字面量枚举。用 curl 分别构造合法与非法请求,观察 400 响应。
  2. 写一个分页列表接口:limitQuery<number> & Min<1> & Max<100>)、offsetQuery<number> & Min<0>),验证越界返回 400。
  3. 把客户端版本从 X-Client-Version 头读进接口,在响应里通过 Header<"X-Server-Time"> 回写服务器时间。
  4. 设计乘机人接口:idNopassportNo 至少传一个——先试试类型层能否表达,体会"结构校验/业务校验"的边界(答案:这属于业务校验,进处理函数)。

八、总结

  1. 接口类型 = 校验 schema = 文档 = 客户端类型,一处定义四处生效。
  2. 校验在 Rust 层执行,非法请求不进入 JavaScript——性能与安全双收益。
  3. 九个校验器覆盖常规约束,&/| 组合表达且/或逻辑。
  4. 参数来源四通道:路径自动映射、Query<>Header<>Cookie<>,其余按方法默认走 body 或查询串;包装类型只在顶层生效。
  5. 结构校验交框架、业务校验留代码,是使用 Encore 校验体系的正确姿势。

请继续阅读:第5章:数据库


原始资料引用



本章目录
一、双重保障:编译时 + 运行时二、支持的类型三、校验器:叠加约束四、四种参数来源五、和 Zod 系方案的对比六、常见误区七、实践练习八、总结原始资料引用Related Documents
苏ICP备2025204887号-2