Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KeDBx2PHLavbURXB8LJJ7m
52 lines
1.5 KiB
TypeScript
52 lines
1.5 KiB
TypeScript
/**
|
|
* 路由契约的形状(编码规范 §8.1 / §9.2)。
|
|
*
|
|
* 前端只经 `shared/typed-fetch` + 本目录的路由契约与服务端交互;组件内禁止裸 fetch。
|
|
* **禁止 Hono RPC 客户端** —— 它要求前端 import 后端类型,违反 §5.1。
|
|
*/
|
|
import type { z } from 'zod';
|
|
|
|
export const HTTP_METHODS = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE'] as const;
|
|
export type HttpMethod = (typeof HTTP_METHODS)[number];
|
|
|
|
/**
|
|
* 一条路由契约。`path` 用 `:name` 表示路径参数,与 `params` 的键一一对应。
|
|
* body 只在 POST/PATCH 上有意义;GET 用 query。
|
|
*/
|
|
export type RouteContract<
|
|
TParams extends z.ZodTypeAny = z.ZodTypeAny,
|
|
TQuery extends z.ZodTypeAny = z.ZodTypeAny,
|
|
TBody extends z.ZodTypeAny = z.ZodTypeAny,
|
|
TOutput extends z.ZodTypeAny = z.ZodTypeAny,
|
|
> = {
|
|
readonly method: HttpMethod;
|
|
readonly path: string;
|
|
readonly params?: TParams;
|
|
readonly query?: TQuery;
|
|
readonly body?: TBody;
|
|
readonly output: TOutput;
|
|
};
|
|
|
|
export type RouteOutput<R extends RouteContract> = z.infer<R['output']>;
|
|
export type RouteBody<R extends RouteContract> = R['body'] extends z.ZodTypeAny
|
|
? z.infer<R['body']>
|
|
: never;
|
|
|
|
/** 结构化错误:前端按 code 分辨,不解析文案 */
|
|
export const API_ERROR_CODES = [
|
|
'unauthenticated',
|
|
'forbidden',
|
|
'not-found',
|
|
'validation-failed',
|
|
'conflict',
|
|
'precondition-failed',
|
|
'internal',
|
|
] as const;
|
|
export type ApiErrorCode = (typeof API_ERROR_CODES)[number];
|
|
|
|
export type ApiError = {
|
|
readonly code: ApiErrorCode;
|
|
readonly message: string;
|
|
readonly detail?: unknown;
|
|
};
|