示例
openapi-typescript生成的类型是通用的,可以以各种方式使用。虽然这些示例不够全面,但希望它们能激发你如何在应用程序中使用这些类型的想法。
数据获取
可以使用自动生成类型的fetch包装器简单而安全地获取数据:
TIP
一个良好的fetch包装器不应使用泛型。泛型需要更多的输入,并且可能隐藏错误!
Hono
Hono 是一个现代的用于 Node.js 的服务器框架,可以轻松部署到网络中(例如 Cloudflare Workers),就像部署到标准容器一样。它还内置了 TypeScript,因此非常适合生成的类型。
在使用 CLI 生成类型之后,为每个端点传递适当的 paths
响应:
import { Hono } from "hono";
import { components, paths } from "./path/to/my/types";
const app = new Hono();
/** /users */
app.get("/users", async (ctx) => {
try {
const users = db.get("SELECT * from users");
return ctx.json<
paths["/users"]["responses"][200]["content"]["application/json"]
>(users);
} catch (err) {
return ctx.json<components["schemas"]["Error"]>({
status: 500,
message: err ?? "An error occurred",
});
}
});
export default app;
TIP
在服务器环境中进行类型检查可能很棘手,因为通常会查询数据库并与 TypeScript 无法内省的其他端点通信。但是使用泛型将使你能够注意到 TypeScript 能够 捕获的明显错误(在你的堆栈中可能有更多具有类型的东西,而你并不了解!)。
Mock-Service-Worker (MSW)
如果你正在使用 Mock Service Worker (MSW) 来定义 API 的模拟数据,你可以使用一个 小巧、自动类型化的封装 来包裹 MSW,这样当你的 OpenAPI 规范发生变化时,你可以轻松解决 API 模拟数据中的冲突。最终,你可以对应用程序的 API 客户端和 API 模拟数据具有相同的信心水平。
使用 openapi-typescript
和一个 fetch 的包装器,比如 openapi-fetch
,可以确保我们应用程序的 API 客户端不会与 OpenAPI 规范冲突。
然而,虽然你可以轻松解决 API 客户端的问题,但你必须手动记住调整 API 模拟,因为没有机制提醒你有冲突。
我们推荐使用以下的包装器,它与 openapi-typescript
完美配合:
测试模拟
测试出现误报的最常见原因之一是模拟数据与实际 API 响应不同步。
openapi-typescript
提供了一种极好的方法来防范这种情况,而且付出的努力很小。下面是一个示例,演示如何编写一个帮助函数,对所有模拟数据进行类型检查以符合你的 OpenAPI 架构(我们将使用 vitest/vitest-fetch-mock,但相同的原理也适用于任何设置):
假设我们想要按照以下对象结构编写模拟数据,以便一次性模拟多个端点:
{
[pathname]: {
[HTTP method]: { status: [status], body: { …[some mock data] } };
}
}
使用我们生成的类型,我们可以推断出任何给定路径 + HTTP 方法 + 状态码的正确数据结构。示例测试如下:
import { mockResponses } from "../test/utils";
describe("My API test", () => {
it("mocks correctly", async () => {
mockResponses({
"/users/{user_id}": {
// ✅ 正确的 200 响应
get: { status: 200, body: { id: "user-id", name: "User Name" } },
// ✅ 正确的 403 响应
delete: { status: 403, body: { code: "403", message: "Unauthorized" } },
},
"/users": {
// ✅ 正确的 201 响应
put: { 201: { status: "success" } },
},
});
// 测试 1: GET /users/{user_id}: 200
await fetch("/users/user-123");
// 测试 2: DELETE /users/{user_id}: 403
await fetch("/users/user-123", { method: "DELETE" });
// 测试 3: PUT /users: 200
await fetch("/users", {
method: "PUT",
body: JSON.stringify({ id: "new-user", name: "New User" }),
});
// 测试清理
fetchMock.resetMocks();
});
});
注意:此示例使用原始的 fetch()
函数,但可以将任何 fetch 包装器(包括 openapi-fetch)直接替换,而不需要进行任何更改。
而能够实现这一点的魔法将存储在 test/utils.ts
文件中,可以在需要的地方复制 + 粘贴(为简单起见进行隐藏):
📄 test/utils.ts
import type { paths } from "./my-openapi-3-schema"; // 由openapi-typescript生成
// 设置
// ⚠️ 重要:请更改这个!这是所有 URL 的前缀
const BASE_URL = "https://myapi.com/v1";
// 结束设置
// 类型帮助程序 —— 忽略这些;这只是使 TS 查找更好的工具,无关紧要。
type FilterKeys<Obj, Matchers> = {
[K in keyof Obj]: K extends Matchers ? Obj[K] : never;
}[keyof Obj];
type PathResponses<T> = T extends { responses: any } ? T["responses"] : unknown;
type OperationContent<T> = T extends { content: any } ? T["content"] : unknown;
type MediaType = `${string}/${string}`;
type MockedResponse<T, Status extends keyof T = keyof T> =
FilterKeys<OperationContent<T[Status]>, MediaType> extends never
? { status: Status; body?: never }
: {
status: Status;
body: FilterKeys<OperationContent<T[Status]>, MediaType>;
};
/**
* 模拟 fetch() 调用并根据 OpenAPI 架构进行类型检查
*/
export function mockResponses(responses: {
[Path in keyof Partial<paths>]: {
[Method in keyof Partial<paths[Path]>]: MockedResponse<
PathResponses<paths[Path][Method]>
>;
};
}) {
fetchMock.mockResponse((req) => {
const mockedPath = findPath(
req.url.replace(BASE_URL, ""),
Object.keys(responses)
)!;
// 注意:这里的类型我们使用了懒惰的方式,因为推断是不好的,而且这有一个 `void` 返回签名。重要的是参数签名。
if (!mockedPath || !(responses as any)[mockedPath])
throw new Error(`No mocked response for ${req.url}`); // 如果未模拟响应,则抛出错误(如果希望有不同的行为,则删除或修改)
const method = req.method.toLowerCase();
if (!(responses as any)[mockedPath][method])
throw new Error(`${req.method} called but not mocked on ${mockedPath}`); // 类似地,如果响应的其他部分没有模拟,则抛出错误
if (!(responses as any)[mockedPath][method]) {
throw new Error(`${req.method} called but not mocked on ${mockedPath}`);
}
const { status, body } = (responses as any)[mockedPath][method];
return { status, body: JSON.stringify(body) };
});
}
// 匹配实际 URL(/users/123)与 OpenAPI 路径(/users/{user_id} 的辅助函数)
export function findPath(
actual: string,
testPaths: string[]
): string | undefined {
const url = new URL(
actual,
actual.startsWith("http") ? undefined : "http://testapi.com"
);
const actualParts = url.pathname.split("/");
for (const p of testPaths) {
let matched = true;
const testParts = p.split("/");
if (actualParts.length !== testParts.length) continue; // 如果长度不同,则自动不匹配
for (let i = 0; i < testParts.length; i++) {
if (testParts[i]!.startsWith("{")) continue; // 路径参数({user_id})始终算作匹配
if (actualParts[i] !== testParts[i]) {
matched = false;
break;
}
}
if (matched) return p;
}
}
补充说明
上面的代码相当复杂!在大多数情况下,这是大量的实现细节,你可以忽略。 mockResponses(…)
函数签名是所有重要的魔法发生的地方,你会注意到这个结构与我们的设计之间有直接的链接。从那里,代码的其余部分只是使运行时按预期工作。
export function mockResponses(responses: {
[Path in keyof Partial<paths>]: {
[Method in keyof Partial<paths[Path]>]: MockedResponse<
PathResponses<paths[Path][Method]>
>;
};
});
现在,每当你的架构更新时,所有的模拟数据都将得到正确的类型检查 🎉。这是确保测试具有弹性和准确性的重要步骤。