光谱
TypeScript 最小箭头功能合同规格系统
零取装饰. 零号DSL. 零从属. 仅仅是 TypeScript 箭头函数 .
部分 ADT 工具包 基础 - 见 主读取器 建筑概况.
哲学
端点 = 箭头函数,其参数定义了合同,而返回值则定义了操作.
import { http } from '@abapify/speci/rest';
// Shortcut syntax - super clean!
const updateUser = (id: string, user: UserInput) =>
http.put<User>(`/users/${id}`, user);
这在保持最低水平和TypeScript-native的同时,表现极强. 在快捷键语法中选择简单或完整的语法来控制.
以 ts-xsd 开发
斯佩克是紧密结合 t-xsd 支助 基于XML的合同虽然大多数REST图书馆都假设JSON, Speci是 Serializable<T> 接口可以与 XML 计划无缝地融合 :
import { parse, build, type XsdSchema, type InferXsd } from 'ts-xsd';
import { http } from '@abapify/speci/rest';
// ts-xsd schema with parse/build
const TransportSchema = {
/* ... */
} as const satisfies XsdSchema;
type Transport = InferXsd<typeof TransportSchema>;
const transportApi = {
// Speci infers types from ts-xsd's Serializable interface
create: (transport: Transport) =>
http.post<Transport>('/transports', transport),
};
这使得Speci对于使用XML(SAP APT,SOAP服务等)的企业API来说是理想的,同时保持与Zod等基于JSON的策略完全兼容.
模块结构
Speci被组织成特定协议模块:
speci- 核心类型和公用事业(议定书不可知性)speci/rest- REST API(帮助者、类型、客户端生成)speci/openapi- OpenAPI生成(计划)speci/cli- 生产(计划)CLIspeci/graphql- 图QL(计划)speci/grpc- gRPC(计划)
每个协议模块都是自成一体的,有自己的类型,帮助者,以及客户端生成.
安装
npm install speci
# or
bun add speci
快速启动
1. 确定合同
import { http, type RestContract } from '@abapify/speci/rest';
// Define schemas (use any schema library: Zod, JSON Schema, etc.)
interface User {
id: string;
name: string;
email: string;
}
interface CreateUserInput {
name: string;
email: string;
}
interface ErrorResponse {
error: string;
message: string;
}
// Define your API contract - choose your style!
export const api = {
users: {
// Shortcut syntax - just specify the success type
list: () => http.get<User[]>('/users'),
// Full syntax - explicit control over all responses
get: (id: string) =>
http.get(`/users/${id}`, {
responses: {
200: undefined as unknown as User,
404: undefined as unknown as ErrorResponse,
},
}),
// Shortcut - pass body directly
create: (user: CreateUserInput) => http.post<User>('/users', user),
// Full syntax with body and responses
update: (id: string, user: Partial<User>) =>
http.put(`/users/${id}`, {
body: user,
responses: {
200: undefined as unknown as User,
404: undefined as unknown as ErrorResponse,
},
}),
// Shortcut - defaults to 204 response
delete: (id: string) => http.delete(`/users/${id}`),
},
} satisfies RestContract;
2. 生成一个输入客户端
import {
createClient,
createFetchAdapter,
HttpError,
} from '@abapify/speci/rest';
const client = createClient(api, {
baseUrl: 'https://api.example.com',
adapter: createFetchAdapter(),
});
// Use it with full type safety - returns only success types
const users = await client.users.list(); // Type: User[]
const user = await client.users.get('123'); // Type: User
// Errors are thrown as HttpError with typed payloads
try {
const user = await client.users.get('999');
} catch (error) {
if (client.users.get.isError(error)) {
// error.payload is typed as ErrorResponse!
console.error(error.status, error.payload.error);
}
}
核心概念
结束点描述符
每个端点由返回描述符的箭头函数定义:
const endpoint = (...params) => ({
method: 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD' | 'OPTIONS',
path: '/path/with/${params}',
body: any, // Request body schema
query: any, // Query parameters schema
headers: any, // Headers schema
responses: {
// Response schemas by status code
200: SuccessSchema,
400: ErrorSchema,
},
metadata: {
// Optional metadata
description: 'Endpoint description',
tags: ['user', 'admin'],
deprecated: false,
},
});
HTTP 帮助对象
这个 @abapify/speci/rest 模块提供 http 所有 HTTP 方法的对象 :
import { http } from '@abapify/speci/rest';
// Shortcut syntax - super clean!
http.get<SuccessType>(path); // GET with 200 response
http.post<SuccessType>(path, body); // POST with 201 response
http.put<SuccessType>(path, body); // PUT with 200 response
http.patch<SuccessType>(path, body); // PATCH with 200 response
http.delete(path); // DELETE with 204 response
// Full syntax - explicit control
http.get(path, { responses: { 200: Type, 404: Error } });
http.post(path, { body, responses: { 201: Type, 400: Error } });
// Examples
const getUser = (id: string) => http.get<User>(`/users/${id}`);
const createUser = (user: CreateUserInput) => http.post<User>('/users', user);
为什么 http 对象? 这个 delete 关键词在 JavaScript 中保留, 因此我们使用 http.delete 而不是独立的 delete 函数.
司马支持
Speci 支持 任何计划库 - 佐德,JSON斯克马,定制计划等等。 使用 schema() 干净语法的助手 :
import { z } from 'zod';
import { schema } from '@abapify/speci/rest';
// Zod schemas
const UserSchema = z.object({
id: z.string(),
name: z.string(),
email: z.string().email(),
});
type User = z.infer<typeof UserSchema>;
const api = {
getUser: (id: string) =>
http.get(`/users/${id}`, {
responses: { 200: schema(UserSchema, {} as User) }, // ✅ Schema + type
}),
};
// Custom schemas (e.g., XML schemas for ADT)
const ClassSchema = {
element: 'class',
attributes: ['name', 'type'],
// ... your schema definition
};
interface ClassXml {
name: string;
type: string;
// ... type definition
}
const adtApi = {
getClass: (name: string) =>
http.get(`/classes/${name}`, {
responses: { 200: schema(ClassSchema, {} as ClassXml) }, // ✅ Schema + type
}),
};
如何运作:
schema(schemaObject, {} as Type)提供运行时间计划和编译时间类型- 适配器 查看 schema 对象,并将其用于分析/验证
- TypeScript 为全类型安全查看类型
- 只类型断言仍然有效 :
undefined as unknown as Type
这让斯佩克 计划不可知 - 用你喜欢的验证库!
全球错误反应
避免在所有端点重复错误类型 :
import { createHttp } from '@abapify/speci/rest';
// 1. Define global error responses
const globalErrors = {
400: undefined as unknown as ApiError,
401: undefined as unknown as ApiError,
403: undefined as unknown as ApiError,
404: undefined as unknown as ApiError,
500: undefined as unknown as ApiError,
} as const;
// 2. Create http instance with global errors
const api = createHttp(globalErrors);
// 3. Now only specify success responses!
const userApi = {
list: () => api.get<User[]>('/users'),
// 400, 401, 403, 404, 500 automatically added!
get: (id: string) => api.get<User>(`/users/${id}`),
// Global errors merged automatically
create: (user: CreateUserInput) => api.post<User>('/users', user),
// All endpoints get global errors
};
路径参数
路径参数自动从模板文字中提取 :
const getPost = (userId: string, postId: string) =>
http.get<Post>(`/users/${userId}/posts/${postId}`);
// Speci automatically maps:
// - First param (userId) → ${userId} in path
// - Second param (postId) → ${postId} in path
处理错误
错误显示为 HttpError 带有打出的有效载荷:
import { HttpError } from '@abapify/speci/rest';
try {
const user = await client.users.get('123');
// user is typed as User (not User | ErrorResponse)
} catch (error) {
// Option 1: Use endpoint-specific type guard
if (client.users.get.isError(error)) {
// error.payload is typed as ErrorResponse
console.error(`HTTP ${error.status}:`, error.payload.error);
}
// Option 2: Generic HttpError check
else if (error instanceof HttpError) {
console.error(`HTTP ${error.status}:`, error.payload);
}
// Option 3: Network or other errors
else {
console.error('Network error:', error);
}
}
自定义 HTTP 适配器
斯佩克是适应者-不可知论者. 带上您的 HTTP 客户端 :
import type { HttpAdapter } from '@abapify/speci/rest';
const myAdapter: HttpAdapter = {
async request({ method, url, body, query, headers }) {
// Use any HTTP client: axios, got, ky, wretch, etc.
return await yourHttpClient.request({ method, url, body, query, headers });
},
};
const client = createClient(api, {
baseUrl: 'https://api.example.com',
adapter: myAdapter,
});
截取器
添加请求/答复/截取器:
const client = createClient(api, {
baseUrl: 'https://api.example.com',
adapter: createFetchAdapter(),
// Add auth token to all requests
onRequest: async (options) => ({
...options,
headers: {
...options.headers,
Authorization: `Bearer ${getToken()}`,
},
}),
// Transform responses
onResponse: async (response) => {
console.log('Response:', response);
return response;
},
// Handle errors
onError: async (error) => {
console.error('Error:', error);
throw error;
},
});
Speci 能够生成的
从你的箭形功能合同中,斯佩克可以生成:
- ✅ 类型客户端 (已执行)
- 🚧 服务器路由 (计划)
- 🚧 OpenAPI 规格 (计划)
- 🚧 模拟服务器 (计划)
- 🚧 测试固定装置 (计划)
- 🚧 CLI 工具 (计划)
- 🚧 图QL 计划 (计划)
- 🚧 gRPC 定义 (计划)
为什么是箭头函数?
✅ 没有类型脚本 AST 解析 - TS 给您本地函数类型
✅ 无装饰器 - 他们是可选的合成糖
✅ 没有模板升解析 - 简单的可变提取
✅ 完全可读 - 看起来像普通域号
✅ 完全可见 - 所有REST/HATEOAS/GRPC概念都可以包起来
✅ 零框架组合 - 纯类型脚本
从Ts -rest来?
Speci使用相同的合同第一哲学,但带有箭头功能:
// ts-rest
const contract = c.router({
getUser: {
method: 'GET',
path: '/users/:id',
responses: { 200: UserSchema },
},
});
// Speci
const contract = {
getUser: (id: string) => http.get<User>(`/users/${id}`),
};
同样的安全性, 少了锅炉板。 选择 ts-rest 表示成熟度, Speci 表示简单和 XML 支持 .
与 ts-rest 比较
ts-resst (英语) 是一个优秀的,成熟的图书馆 用于类型的安全 REST 合同 我们推荐它 用于大多数使用案例。 斯佩克涵盖您需要的特定情景: 动态路由作为函数 (终点定义为有参数的箭头函数), 方案的灵活性 (通过适配器,而不仅仅是Zod)和a 灵活插件系统 用于自定义扩展。 如果Ts -rest的第一合同方法对你有用,就使用它——它经过了战斗测试和生产准备.
与其他工具的比较
| 特性 | 标本 | ts-resst (英语) | tRPC 主题 | 打开API |
|---|---|---|---|---|
| 语法 | 箭头函数 | 构建器 API | 程序 | 也门/日本 |
| 类型安全 | 满分 | 满分 | 满分 | 编码源 |
| 依赖关系 | 0 个 | 少数 | 许多 | 许多 |
| 学习曲线 | 最小值 | 低级 | 中型 | 高级 |
| 客户端 | 对 | 对 | 对 | 对 |
| 服务器 | 计划数 | 对 | 对 | * 部分内容 |
| 验证 | 计划数 | 祖德 苏德 | 祖德 苏德 | 变相 |
文档
- 体参数推论 - 从计划自动推断身体类型
许可证
麻省理工学院
捐款
欢迎捐款! 这是v0.1 - 最小可行的核心。 未来版本将增加:
- 服务器适配器(Express, Fastify, Hono等)
- OpenAPI 生成
- 模拟服务器生成
- Schema验证(佐德,JSON Schema等)
- 还有更多!