使用 Deno 编写一个小型 API

2025-05-25

使用 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 对象具有一些方法(例如jsonsend)来返回响应。在 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,TextEncoderJSON等对象是标准 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
PREV
Git hook 是 Husky 的绝佳替代品
NEXT
Reactjs | 完美用例的完美工具🛠️💥