从头开始创建一个自动产生文档/类型安全的现代API(9) 建立校验规则/按id读取任务

下面我们给 API 参数添加校验功能。

在Schema中添加规则

修改 db/schema.ts:

1
2
3
4
5
6
7
8
9
10
11
12
...
export const insertTasksSchema = createInsertSchema(
tasks,
{
name: z.string().min(1).max(255),
done: z.boolean(),
},
).omit({
id: true,
createdAt: true,
updatedAt: true,
});

这里我添加了规则,name的长度只能在1到255之间,规避了0长度和过长的name。

访问 localhost:3000/reference, 可以看到API已更新:

More...

从头开始创建一个自动产生文档/类型安全的现代API(8) 创建数据

下面我们给API添加创建功能。

添加路径

修改文件 app/api/[[...route]]/routes/tasks/tasks.routes.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
export const create = createRoute({
tags: ["Tasks"],
path: "/tasks",
method: "post",
request: {
body: jsonContentRequired(
insertTasksSchema,
"The task to create",
)
},
responses: {
[HttpStatusCodes.OK]: jsonContent(
selectTasksSchema,
"The created task",
),
},
});

export type CreateRoute = typeof create;
...
More...

如何设置 Drizzle Schema 让 SQLite 支持自动填入创建时间

背景

之前用下面的Schema创建了表:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
export const tasks = sqliteTable("tasks", {
id: integer("id", { mode: "number" })
.primaryKey({ autoIncrement: true }),
name: text("name")
.notNull(),
done: integer("done", { mode: "boolean" })
.notNull()
.default(false),
createdAt: integer("created_at", { mode: "timestamp" })
.$defaultFn(() => new Date()),
updatedAt: integer("updated_at", { mode: "timestamp" })
.$defaultFn(() => new Date())
.$onUpdate(() => new Date()),
});

发现这个创建时间和更新时间并没有生效,研究了一下,这样写可能有问题。

解决方案

查询了一下,修改为下面的语句:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { sql } from "drizzle-orm";
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { createSelectSchema } from "drizzle-zod";

export const tasks = sqliteTable("tasks", {
id: integer("id", { mode: "number" })
.primaryKey({ autoIncrement: true }),
name: text("name")
.notNull(),
done: integer("done", { mode: "boolean" })
.notNull()
.default(false),
createdAt: text('created_at')
.notNull()
.default(sql`(current_timestamp)`),
updatedAt:text('updated_at')
.notNull()
.default(sql`(current_timestamp)`)
});

export const selectTasksSchema = createSelectSchema(tasks);

updateAt字段无效,需要更新时主动更新。

效果

修改完后,利用 drizzle-kit 重新生成数据库:

1
2
bun drizzle-kit generate
bun drizzle-kit push

试了一下,插入数据后自动产生了时间:

1
1|Hello Hone|0|2025-02-24 06:13:34|2025-02-24 06:13:34
More...

从头开始创建一个自动产生文档/类型安全的现代API(7) 读数据库

之前API的实现只是简单的返回固定值,本章开始实行数据库读写。

安装Drizzle

Drizzle ORM 是用于 SQL 数据库的 TypeScript ORM,在设计时考虑到了最大的类型安全性。它带有用于自动生成 SQL 迁移的 drizzle-kit CLI 。Drizzle ORM 是一个库,而不是一个框架,它的主要哲学是 “如果你知道 SQL,你就知道 Drizzle ORM”,因此设计的时候尽可能遵循类似 SQL 的语法,强类型化并在编译时就会失败,而不是在运行时。
安装 Drizzle, Drizzle 的安装根据支持的数据库,命令略有不同,这里我们采用 SQLite , 命令如下:

1
2
3
bun add drizzle-orm @libsql/client dotenv
bun add -D drizzle-kit tsx
bun add drizzle-zod

注:drizzle-zod 是Zod搭配drizzle的一个包,后面会用到。

定义环境变量

修改 utility/env.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
import { z } from "zod";
import { expand } from 'dotenv-expand';
import { config } from 'dotenv';

expand(config());

const EnvSchema = z.object({
NODE_ENV: z.string().default("development"),
PORT: z.coerce.number().default(9999),
LOG_LEVEL: z.enum(["fatal","error","warn", "info", "debug","trace"]),
DATABASE_URL: z.string().url(),
DATABASE_AUTH_TOKEN: z.string().optional(),
});

export type env = z.infer<typeof EnvSchema>;
// eslint-disable-next-line ts/no-redeclare
const parsedEnv = EnvSchema.safeParse(process.env);

if ( parsedEnv.success === false ) {
console.error("❌ Invalid env:");
console.error(JSON.stringify(parsedEnv.error.flatten().fieldErrors, null, 2));
process.exit(1);
}

const env = parsedEnv.data;

export default env;
More...

从头开始创建一个自动产生文档/类型安全的现代API(6) 创建子路径

创建子路径

上一篇我们创建了 api/节点,这次我们继续创建子节点,通过在Routes路径下创建子目录和文件实现。
首先, 添加 app/api/[[...route]]/routes/tasks/tasks.index.ts:

1
2
3
4
5
6
7
8
9
import { createRouter } from "../../lib/create-app";

import * as handlers from "./tasks.handlers";
import * as routes from "./tasks.routes";

const router = createRouter()
.openapi(routes.list, handlers.list);

export default router;

这个文件作为索引文件,链接了handler和router两个文件,我们来创建它们。

More...

从头开始创建一个自动产生文档/类型安全的现代API(5) 文档生成

添加Doc支持

添加 app/api/[[...route]]/lab/configure-openapi.ts:

1
2
3
4
5
6
7
8
9
10
11
12
import type { AppOpenAPI } from "@/utility/types";
import packageJSON from "@/package.json" with { type: "json" };

export default function configureOpenAPI(app : AppOpenAPI){
app.doc("/doc", {
openapi: "3.0.0",
info: {
version: packageJSON.version,
title: "Tasks API",
},
});
}

修改 app/api/[[...route]]/route.ts,添加调用:

1
2
3
4
5
6
7
...
import configureOpenAPI from './lab/configure-openapi';

const app = createApp();

configureOpenAPI(app);
...

访问 网址 localhost:3001/api/doc,显示如下:

More...

从头开始创建一个自动产生文档/类型安全的现代API(4) 启用Zod/代码重组

启用Zod

Zod 是一个 TypeScript 优先的模式声明和验证库。我使用术语 “模式” 来广义地指任何数据类型,从简单的 字符串 到复杂的嵌套对象。Zod 围绕尽可能友好的开发体验而设计。其目的是消除重复的类型声明。使用 Zod,你只需声明 一次 验证器,Zod 就会自动推断出静态 TypeScript 类型。将简单类型组合成复杂的数据结构非常容易。我的理解,它补足了 Typescript 在动态类型检查上的缺陷,让程序可以更稳定的运行,减少因为类型不匹配导致的问题。
可以用 bun add zod 安装,但实际上我们之前因为依赖,已经安装过了。

创建Zod Schema

创建文件 utility/env.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { z } from "zod";
import { expand } from 'dotenv-expand';
import { config } from 'dotenv';

expand(config());

const EnvSchema = z.object({
NODE_ENV: z.string().default("development"),
PORT: z.coerce.number().default(9999),
LOG_LEVEL: z.enum(["fatal","error","warn", "info", "debug","trace"]),
});

export type env = z.infer<typeof EnvSchema>;
// eslint-disable-next-line ts/no-redeclare
const { data: env, error } = EnvSchema.safeParse(process.env);

if (error) {
console.error("❌ Invalid env:");
console.error(JSON.stringify(error.flatten().fieldErrors, null, 2));
process.exit(1);
}

export default env;

其他代码中用到 process.env.xxx 都改成 env.xxx

More...

从头开始创建一个自动产生文档/类型安全的现代API(3) 日志搭建 - Bearalise

搭建日志平台Pino

Pino 是一个非常快速且简洁的 Node.js 日志库,其设计宗旨在于提供最小的开销以及高性能的日志记录功能。下面我们来在项目中搭建它。
安装:

1
bun add hono-pino pino

代码实现:
添加文件:middlewares/pino-logger.ts:

1
2
3
4
5
import { pinoLogger } from 'hono-pino';

export function pnLogger() {
return pinoLogger();
}

运行程序:bun run dev
访问localhost,返回 “Hello Hono”,同时控制台显示:
apidemo4

More...

从头开始创建一个自动产生文档/类型安全的现代API(2) 安装Zod OpenAPI Hono/stoker - Bearalise

搭建API路径并运行

在项目目录下创建文件:app/api/[[...route]]/route.ts:
apidemo1

1
2
3
4
5
6
7
8
9
10
11
12
13
import { Hono } from "hono";
import { handle } from 'hono/vercel';

const app = new Hono().basePath('/api');

app.get("/", (c) => {
return c.text("Hello Hono");
});

const port = 3001;
console.log(`Server running on port ${port}`);

export const GET = handle(app);

运行程序:bun run dev
访问localhost,返回 “Hello Hono”,同时控制台显示:

1
Server running on port 3001
More...

从头开始创建一个自动产生文档/类型安全的现代API(1) 创建项目/Eslint设置 - Bearalise

背景

你是否曾经遇到过这样的 API 文档,其中每一个端点(endpoint)都被详细记录,你可以深入了解所有可能的响应类型,并查看它们的示例。同时,它们还包含模式(schemas),你甚至可以进入并测试这些特定的端点,实际运行并访问 API,设置请求头和其他细节。从这个文档开始,我将从头开始构建一个使用 Hono 的 API,并使用 OpenAPI 规范来全面记录我们的 API。因此,如果你访问 /doc,你会看到每一个端点都被记录在案。OpenAPI 实际上是一个工具生态系统,可以利用这些文档创建互动文档,或者生成客户端库及其他类似的东西。我一直在使用 Hono 构建应用程序,特别是我一直在使用他们的中间件 Zod OpenAPI,该中间件基本上允许你使用 Zod 来定义所有的模式(schemas),然后定义你的路由合约,并使用这些路由合约在定义请求处理程序时获得类型安全。所有这些最终都被转化为完全互动的文档。

技术实现简介

首先,我会使用NextJS和Hono搭建后台API。NextJS不用多介绍,它是一个用于构建全栈Web 应用的React 框架。Hono是一个小巧、简单且超快速的 Web 框架,构建在 Web 标准之上,号称可以在任何 JavaScript 运行时环境中运行,包括 Cloudflare Workers、Fastly Compute、Deno、Bun、Vercel、Netlify、AWS Lambda、Lambda@Edge 和 Node.js。

  • 在设置 OpenAPI 时,我们也可以以一种可扩展的方式进行。例如,我们可以在一个地方设置所有的路由定义,然后在另一个地方设置所有的处理程序,但一切依然是类型安全的。我们实际上会导出这些路由定义的类型,并且可以使用它们来定义我们的请求处理程序。这样,我们可以在一个单独的文件中定义我们的处理程序,但仍然具有完整的类型安全性,这意味着我们准确知道对于这个特定的方法基于这个特定的路由我们应该响应什么。

  • 除此之外,Hono 使得测试这些端点变得非常容易,因为他们有一个内置的测试客户端,本质上类似于 Hono RPC 客户端,可以在客户端代码中使用,并具有完全的类型安全性,但它也是一个非常好的测试体验。

  • 此外,我们可以将其与 Drizzle 连接起来,Drizzle Zod 基本上允许我们定义我们的表,然后使用这些表定义来定义我们的 Zod 模式(schemas),然后我们可以直接在我们的 OpenAPI 定义中使用这些模式。因此,我们基本上有一个单一的事实来源,我们可以定义我们的表,得到我们的模式,然后在我们的路由定义中使用这些模式,从那时起,当我们定义处理程序时,所有类型都会流转。

其次,我使用了一些现成的库 stoker 和样板库 hono-open-api-starter 来减少代码量,当然你也可以直接自己实现。

More...

请我喝杯咖啡吧~

支付宝
微信