使用 Deno 编写一个小型 API
在这篇文章中,我将向您展示如何使用Deno创建一个小型 API - Deno 是由 Node.js 的作者 Ryan Dahl 创建的运行 Javascript 和 Typescript 的最新运行时。
如果你不知道 Deno 是什么,请查看这篇文章:Deno 入门。
我们的目标是:
- 创建一个管理用户的 API
- 提供 GET、POST、PUT 和 DELETE 路由
- 将创建/更新的用户保存到本地 JSON 文件
- 使用 Web 框架来加速开发过程
您唯一需要安装的工具就是 Deno 本身。Deno 开箱即用地支持 Typescript。在本例中,我使用了 0.22 版本。Deno API 仍在持续开发中,因此此代码可能无法兼容其他版本。请在终端中使用deno version命令检查您的版本。
让我们开始吧
您可以在 Github 上找到以下代码:github.com/kryz81/deno-api-example
步骤 1:程序结构
handlers
middlewares
models
services
config.ts
index.ts
routing.ts
如您所见,它看起来像一个小型的 Node.js Web 应用程序:
- handlers包含路由处理程序
- 中间件提供在每个请求上运行的功能
- 模型包含模型定义,在我们的例子中只有用户界面
- 服务包含...服务
- config.ts包含全局应用程序配置
- index.ts是应用程序的入口点
- routes.ts包含 API 路由
第 2 步:选择 Web 框架
Node.js 有很多优秀的 Web 框架。其中最受欢迎的是Express。此外,还有一个 Express 的现代版本—— Koa。但是 Deno 与 Node.js 不兼容,我们无法使用 Node.js 的库。就 Deno 而言,目前的选择范围要小得多,但有一个受 Koa 启发的框架—— Oak。我们用它作为示例。如果你从未使用过 Koa,不用担心,它看起来几乎与 Express 相同。
步骤 3:创建主文件
索引.ts
import { Application } from "https://deno.land/x/oak/mod.ts";
import { APP_HOST, APP_PORT } from "./config.ts";
import router from "./routing.ts";
import notFound from "./handlers/notFound.ts";
import errorMiddleware from "./middlewares/error.ts";
const app = new Application();
app.use(errorMiddleware);
app.use(router.routes());
app.use(router.allowedMethods());
app.use(notFound);
console.log(`Listening on ${APP_PORT}...`);
await app.listen(`${APP_HOST}:${APP_PORT}`);
第一行代码使用了 Deno 的特性——直接从网络导入模块。除此之外,没有什么特别的。我们创建一个应用程序,添加中间件、路由,最后启动服务器。就像在 Express/Koa 中一样。
步骤 4:创建配置
配置.ts
const env = Deno.env();
export const APP_HOST = env.APP_HOST || "127.0.0.1";
export const APP_PORT = env.APP_PORT || 4000;
export const DB_PATH = env.DB_PATH || "./db/users.json";
我们的配置非常灵活,设置从环境中读取,但我们也提供开发过程中使用的默认值。Deno.env ()相当于 Node.js 中的process.env。
步骤5:添加用户模型
模型/用户.ts
export interface User {
id: string;
name: string;
role: string;
jiraAdmin: boolean;
added: Date;
}
我们需要这个界面来进行正确的输入。
步骤 6:添加路线
路由.ts
import { Router } from "https://deno.land/x/oak/mod.ts";
import getUsers from "./handlers/getUsers.ts";
import getUserDetails from "./handlers/getUserDetails.ts";
import createUser from "./handlers/createUser.ts";
import updateUser from "./handlers/updateUser.ts";
import deleteUser from "./handlers/deleteUser.ts";
const router = new Router();
router
.get("/users", getUsers)
.get("/users/:id", getUserDetails)
.post("/users", createUser)
.put("/users/:id", updateUser)
.delete("/users/:id", deleteUser);
export default router;
同样,没什么特别的,我们创建一个路由器并添加路由。它看起来几乎就像从 Express.js 应用程序中复制粘贴过来的!
步骤 7:添加路由处理程序
处理程序/getUsers.ts
import { getUsers } from "../services/users.ts";
export default async ({ response }) => {
response.body = await getUsers();
};
它返回所有用户。如果您从未使用过 Koa,那么响应对象类似于Express 中的res 。Express 中的 res 对象具有一些方法(例如json或send)来返回响应。在 Koa/Oak 中,我们需要将响应值附加到request.body属性。
处理程序/getUserDetails.ts
import { getUser } from "../services/users.ts";
export default async ({ params, response }) => {
const userId = params.id;
if (!userId) {
response.status = 400;
response.body = { msg: "Invalid user id" };
return;
}
const foundUser = await getUser(userId);
if (!foundUser) {
response.status = 404;
response.body = { msg: `User with ID ${userId} not found` };
return;
}
response.body = foundUser;
};
它返回具有给定 id 的用户。
处理程序/createUser.ts
import { createUser } from "../services/users.ts";
export default async ({ request, response }) => {
if (!request.hasBody) {
response.status = 400;
response.body = { msg: "Invalid user data" };
return;
}
const {
value: { name, role, jiraAdmin }
} = await request.body();
if (!name || !role) {
response.status = 422;
response.body = { msg: "Incorrect user data. Name and role are required" };
return;
}
const userId = await createUser({ name, role, jiraAdmin });
response.body = { msg: "User created", userId };
};
该处理程序管理用户创建。
处理程序/updateUser.ts
import { updateUser } from "../services/users.ts";
export default async ({ params, request, response }) => {
const userId = params.id;
if (!userId) {
response.status = 400;
response.body = { msg: "Invalid user id" };
return;
}
if (!request.hasBody) {
response.status = 400;
response.body = { msg: "Invalid user data" };
return;
}
const {
value: { name, role, jiraAdmin }
} = await request.body();
await updateUser(userId, { name, role, jiraAdmin });
response.body = { msg: "User updated" };
};
更新处理程序检查具有给定 ID 的用户是否存在并更新用户数据。
处理程序/deleteUser.ts
import { deleteUser, getUser } from "../services/users.ts";
export default async ({ params, response }) => {
const userId = params.id;
if (!userId) {
response.status = 400;
response.body = { msg: "Invalid user id" };
return;
}
const foundUser = await getUser(userId);
if (!foundUser) {
response.status = 404;
response.body = { msg: `User with ID ${userId} not found` };
return;
}
await deleteUser(userId);
response.body = { msg: "User deleted" };
};
该处理程序删除一个用户。
我们还想处理非退出路线并返回错误消息:
处理程序/notFound.ts
export default ({ response }) => {
response.status = 404;
response.body = { msg: "Not Found" };
};
步骤 8:添加服务
在创建用户服务之前,我们需要创建两个小型辅助服务。
服务/createId.ts
import { v4 as uuid } from "https://deno.land/std/uuid/mod.ts";
export default () => uuid.generate();
每个新用户都会获得一个唯一的 ID,为此,我们将使用Deno 标准库中的uuid模块。
服务/db.ts
import { DB_PATH } from "../config.ts";
import { User } from "../models/user.ts";
export const fetchData = async (): Promise<User[]> => {
const data = await Deno.readFile(DB_PATH);
const decoder = new TextDecoder();
const decodedData = decoder.decode(data);
return JSON.parse(decodedData);
};
export const persistData = async (data): Promise<void> => {
const encoder = new TextEncoder();
await Deno.writeFile(DB_PATH, encoder.encode(JSON.stringify(data)));
};
此服务帮助我们与虚假用户的存储进行交互,在我们的例子中,这是一个本地 json 文件。为了获取用户,我们读取文件内容。readFile函数返回一个Uint8Array对象,需要先将其转换为字符串,然后才能解析为JSON。 Uint8Array 和 TextDecoder 都来自核心 Javascript API 。同样,要持久化的数据也需要从字符串转换为Uint8Array。
最后,这是负责管理用户数据的主要服务:
服务/用户.ts
import { fetchData, persistData } from "./db.ts";
import { User } from "../models/user.ts";
import createId from "../services/createId.ts";
type UserData = Pick<User, "name" | "role" | "jiraAdmin">;
export const getUsers = async (): Promise<User[]> => {
const users = await fetchData();
// sort by name
return users.sort((a, b) => a.name.localeCompare(b.name));
};
export const getUser = async (userId: string): Promise<User | undefined> => {
const users = await fetchData();
return users.find(({ id }) => id === userId);
};
export const createUser = async (userData: UserData): Promise<string> => {
const users = await fetchData();
const newUser: User = {
id: createId(),
name: String(userData.name),
role: String(userData.role),
jiraAdmin: "jiraAdmin" in userData ? Boolean(userData.jiraAdmin) : false,
added: new Date()
};
await persistData([...users, newUser]);
return newUser.id;
};
export const updateUser = async (
userId: string,
userData: UserData
): Promise<void> => {
const user = await getUser(userId);
if (!user) {
throw new Error("User not found");
}
const updatedUser = {
...user,
name: userData.name !== undefined ? String(userData.name) : user.name,
role: userData.role !== undefined ? String(userData.role) : user.role,
jiraAdmin:
userData.jiraAdmin !== undefined
? Boolean(userData.jiraAdmin)
: user.jiraAdmin
};
const users = await fetchData();
const filteredUsers = users.filter(user => user.id !== userId);
persistData([...filteredUsers, updatedUser]);
};
export const deleteUser = async (userId: string): Promise<void> => {
const users = await getUsers();
const filteredUsers = users.filter(user => user.id !== userId);
persistData(filteredUsers);
};
这里有很多代码,但它是标准的打字稿。
步骤9:添加错误处理中间件
如果用户服务出错,最糟糕的情况会是什么?整个程序都会崩溃。为了避免这种情况,我们可以在每个处理程序中添加try/catch块,但还有一个更好的解决方案——在所有路由之前添加一个中间件,并在那里捕获所有意外错误。
中间件/error.ts
export default async ({ response }, next) => {
try {
await next();
} catch (err) {
response.status = 500;
response.body = { msg: err.message };
}
};
步骤 10:添加示例数据
在运行程序之前,我们将添加一些示例数据。
db/users.json
[
{
"id": "1",
"name": "Daniel",
"role": "Software Architect",
"jiraAdmin": true,
"added": "2017-10-15"
},
{
"id": "2",
"name": "Markus",
"role": "Frontend Engineer",
"jiraAdmin": false,
"added": "2018-09-01"
}
]
就这样。太棒了!现在我们可以运行 API 了:
deno -A index.ts
“A”标志表示我们无需手动授予程序运行的权限。出于开发目的,我们将允许所有权限。请记住,在生产环境中这样做并不安全。
您应该会看到很多下载和编译行,最后我们看到:
Listening on 4000...
概括
我们使用了什么:
- 用于写入和读取文件的全局Deno对象
- Deno 标准库中的uuid用于创建唯一 id
- oak - 受 Node.js Koa 框架启发的第三方框架
- 其余的是纯 TypeScript,TextEncoder或JSON等对象是标准 Javascript 对象
这与 Node.js 有何不同:
- 我们不需要安装和配置 TypeScript 编译器或其他工具(例如 ts-node),只需使用deno index.ts即可运行程序。
- 我们直接在代码中导入所有外部模块,无需在开始实现应用程序之前安装它们
- 没有 package.json 和 package-lock.json
- 程序根目录中没有node_modules;我们的文件存储在全局缓存中
您可以在此处找到完整的源代码:https://github.com/kryz81/deno-api-example
您有任何疑问吗?如有,请在下方留言。如果您喜欢这篇文章,请转发。
文章来源:https://dev.to/kryz/write-a-small-api-using-deno-1cl0