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

第3章:服务与 API —— 类型化端点、路径参数与错误处理

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

API 是 Encore 的第一原语。本章覆盖 api() 的全部选项、四种签名、路径语法、自定义状态码、17 个标准错误码,以及 api.rawapi.static 两个特殊形态。


一、服务 (Service)

服务是 Encore 的部署与组织单元:一个目录 + 一个 encore.service.ts = 一个服务

typescript
// order/encore.service.ts
import { Service } from "encore.dev/service";
export default new Service("order");

规则:

  • 服务名小写、能描述领域(orderpayment,不要 service1);
  • 服务不能嵌套——order/refund/encore.service.ts 会报错;服务内部组织代码用普通子目录;
  • 目录下所有 .ts 文件里被 api() 包装的导出,自动归属这个服务。

单服务还是多服务?第 6 章给决策表;起步建议先单服务,出现明确边界再拆

二、api():类型化端点

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

interface CreateOrderRequest {
  flightNo: string;
  passengerName: string;
}

interface CreateOrderResponse {
  orderId: string;
  status: string;
}

// 创建订单
export const createOrder = api(
  { method: "POST", path: "/orders", expose: true },
  async (req: CreateOrderRequest): Promise<CreateOrderResponse> => {
    // 业务逻辑
    return { orderId: "ORD123", status: "created" };
  },
);

一个端点 = 选项对象 + 类型化异步函数。请求/响应类型就是普通 TypeScript 接口——它们同时是:运行时校验 schema(Rust 层执行)、API 文档、生成客户端的类型来源。

全部选项

选项类型说明
methodstringHTTP 方法:GET / POST / PUT / PATCH / DELETE,"*" 匹配所有
pathstringURL 路径,支持 :param*wildcard;省略则默认 /服务名.函数名
exposeboolean默认 false:仅服务间可调。true 才对公网开放
authbooleantrue 要求请求通过鉴权处理器(第 10 章)
sensitivebooleantrue 时该端点的请求/响应在追踪中自动脱敏——支付、实名信息类接口必配
tagsstring[]打标签,供中间件按标签定向(第 12 章)

expose: false 默认私有是安全设计 内部 API 想被外网调必须显式声明。对照传统框架"写了路由就是公网可达",这条默认值直接消灭一类误暴露事故。值得抄进任何技术栈的团队规范。

四种签名

typescript
// 1. 请求 + 响应
export const createUser = api(
  { method: "POST", path: "/users", expose: true },
  async (req: CreateRequest): Promise<CreateResponse> => { /* ... */ },
);

// 2. 只有响应(GET 列表类)
export const listUsers = api(
  { method: "GET", path: "/users", expose: true },
  async (): Promise<ListResponse> => { /* ... */ },
);

// 3. 只有请求(删除类)
export const deleteUser = api(
  { method: "DELETE", path: "/users/:id", expose: true },
  async (req: DeleteRequest): Promise<void> => { /* ... */ },
);

// 4. 都没有(健康检查类)
export const ping = api(
  { method: "GET", path: "/ping", expose: true },
  async (): Promise<void> => { /* ... */ },
);

也可以用泛型参数形式 api<Req, Resp>(options, handler),两种写法等价。

三、路径参数

typescript
// :param —— 单段占位,映射到请求接口的同名字段
export const getOrder = api(
  { method: "GET", path: "/orders/:id", expose: true },
  async ({ id }: { id: string }): Promise<Order> => { /* ... */ },
);

// *wildcard —— 通配剩余所有段
export const proxy = api(
  { method: "GET", path: "/files/*path", expose: true },
  async ({ path }: { path: string }): Promise<FileInfo> => { /* ... */ },
);

路径字段类型可以是 stringnumber(自动转换并校验)。路径、查询串、请求头、请求体四种参数来源的完整规则在第 4 章。

四、自定义 HTTP 状态码

默认成功返回 200。需要别的状态码时,在响应接口里加一个 HttpStatus 类型字段:

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

interface CreateResponse {
  id: string;
  status: HttpStatus;
}

export const create = api(
  { method: "POST", path: "/items", expose: true },
  async (req: CreateRequest): Promise<CreateResponse> => {
    const item = await createItem(req);
    return { id: item.id, status: HttpStatus.Created };  // 返回 201
  },
);

五、错误处理:APIError 与 17 个标准错误码

Encore 的错误约定:APIError,不要返回错误对象

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

// 完整形式
throw new APIError(ErrCode.NotFound, "order not found");

// 简写(推荐)
throw APIError.notFound("order not found");
throw APIError.invalidArgument("flightNo is required");
throw APIError.unauthenticated("invalid token");

客户端收到的统一错误格式:

json
{
  "code": "not_found",
  "message": "order not found",
  "details": null
}

错误码全表(名称 → 线上字符串 → HTTP 状态)

ErrCode字符串HTTP典型场景
OKok200
Canceledcanceled499客户端断开
Unknownunknown500未归类错误
InvalidArgumentinvalid_argument400参数不合法
DeadlineExceededdeadline_exceeded504超时
NotFoundnot_found404资源不存在
AlreadyExistsalready_exists409重复创建
PermissionDeniedpermission_denied403已登录但无权限
ResourceExhaustedresource_exhausted429限流/配额
FailedPreconditionfailed_precondition400状态机不允许(如"未支付订单不能退票")
Abortedaborted409并发冲突
OutOfRangeout_of_range400越界
Unimplementedunimplemented501未实现
Internalinternal500内部错误
Unavailableunavailable503依赖不可用
DataLossdata_loss500数据损坏
Unauthenticatedunauthenticated401未登录/凭据无效

附加结构化细节用 withDetails

typescript
throw APIError.failedPrecondition("order not payable").withDetails({
  orderId: id,
  currentStatus: "CANCELLED",
});

业务错误码选型经验 订单状态不允许操作用 FailedPrecondition 而不是 InvalidArgument——参数本身是合法的,是状态不满足前置条件。并发写冲突用 Aborted。这套错误码源自 Google API 设计规范 (google.rpc.Code),跨语言通用。

六、api.raw:接管原始 HTTP

需要直接控制请求/响应时(第三方 webhook 验签、自定义响应格式、接 GraphQL 服务器)用 api.raw,签名是 Node.js 风格的 (req, resp)

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

// 接收支付网关回调:需要读原始 body 验签
export const paymentCallback = api.raw(
  { expose: true, path: "/callback/payment", method: "POST" },
  async (req, resp) => {
    const chunks: Buffer[] = [];
    for await (const chunk of req) chunks.push(chunk as Buffer);
    const rawBody = Buffer.concat(chunks).toString("utf8");

    if (!verifySignature(req.headers["x-signature"] as string, rawBody)) {
      resp.writeHead(401);
      resp.end("invalid signature");
      return;
    }
    // ……处理业务……
    resp.writeHead(200, { "Content-Type": "application/json" });
    resp.end(JSON.stringify({ received: true }));
  },
);

raw 端点绕过类型校验,但仍在追踪与中间件覆盖内。GraphQL 接入(Apollo Server 挂在 raw 端点上)官方有完整模板,属于同一模式。

七、api.static:静态文件与 SPA 托管

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

// 前缀托管:./assets 下的文件挂到 /static/* 下
export const assets = api.static(
  { expose: true, path: "/static/*path", dir: "./assets" },
);

// 根路径兜底托管(SPA 场景):注意是 !path 不是 *path
export const frontend = api.static(
  { expose: true, path: "/!path", dir: "./dist", notFound: "./dist/index.html" },
);
  • *path:标准通配,占住整个前缀;
  • !path兜底路由——只接住没有被任何其他 API 匹配的路径,因此可以安全挂在根路径 /,单页应用 (SPA) 前端和 API 共存一个域名;
  • notFound:自定义 404 页面;目录根的 index.html 自动服务。

静态目录是构建期依赖(本机 B2B 项目踩坑) api.static 声明的 dirnotFound 文件在静态分析期就必须存在——干净 clone 下来没有前端构建产物时,整个应用直接构建失败。解法:把占位 index.html 提交进仓库。

八、跨服务调用预告

其他服务的 API 通过 ~encore/clients 以函数调用形式使用(第 6 章详解):

typescript
import { order } from "~encore/clients";
const detail = await order.getOrder({ id: "ORD123" });

九、常见误区

  1. 忘写 expose: true 然后奇怪"为什么 curl 404"——默认私有。
  2. 用返回值传错误({ success: false })——破坏错误码语义、客户端类型和追踪标记,一律抛 APIError
  3. 所有 4xx 都用 invalidArgument——状态类错误应该用 FailedPrecondition / Aborted,见错误码表。
  4. 支付/实名接口忘了 sensitive: true——载荷会被明文记进 trace。
  5. api.raw 逃避类型系统写普通业务接口——raw 只留给 webhook / 特殊响应格式。
  6. SPA 托管用 *path 挂根路径——会吃掉所有 API 路由,必须用 !path

十、实践练习

  1. 写一个 flight 服务,含 GET /flights/:flightNo(查航班)与 POST /flights(录入航班),在 API Explorer 里调通。
  2. 给查询端点加"航班不存在→APIError.notFound"与"航班号格式非法→invalidArgument"两种错误,用 curl 验证响应体里的 code 字段。
  3. 创建接口返回 201:给响应加 HttpStatus.Created
  4. 写一个 api.raw 端点接收模拟支付回调,读取原始 body 并原样回显 JSON。
  5. 把任意前端构建产物(或一个手写 index.html)用 api.static + /!path 挂到根路径,确认它与 /flights API 共存不冲突。

十一、总结

  1. 服务 = 目录 + encore.service.ts,不可嵌套;API = api(选项, 类型化异步函数)
  2. 六个选项里最重要的三个:expose 默认私有、auth 接鉴权、sensitive 追踪脱敏。
  3. 请求/响应接口一式三用:运行时校验、文档、客户端类型。
  4. 错误处理只有一条路:抛 APIError,17 个标准码映射 HTTP 状态,withDetails 带结构化细节。
  5. api.raw 接管原始 HTTP(webhook/GraphQL),api.static 托管静态资源(SPA 用 !path 兜底)。

请继续阅读:第4章:请求校验


原始资料引用



本章目录
一、服务 (Service)二、api():类型化端点三、路径参数四、自定义 HTTP 状态码五、错误处理:APIError 与 17 个标准错误码六、api.raw:接管原始 HTTP七、api.static:静态文件与 SPA 托管八、跨服务调用预告九、常见误区十、实践练习十一、总结原始资料引用Related Documents
苏ICP备2025204887号-2