创建一个小的Deno API

在这篇文章中,我想讲述和展示使用Deno创建小型API的过程。Deno是由Node.js创建者Ryan Dahl开发的最新Javascript和Typescript启动器。



我们的目标

  • 开发一个可以处理用户数据的API
  • 提供使用GET,POST,PUT和DELETE方法的能力
  • 在本地JSON文件中保存和更新用户数据
  • 使用框架加快开发速度

我们唯一需要安装的是Deno。Deno开箱即用地支持Typescript。对于此示例,我使用了Deno版本0.22,并且此代码可能无法在以后的版本中使用。
可以在终端中使用deno version命令找到已安装的Deno的版本

程序结构


handlers
middlewares
models
services
config.ts
index.ts
routing.ts

如您所见,它看起来像Node.js上的小型Web应用程序。

  • 处理程序 -包含路由处理程序
  • 中间件 -包含将在每次请求时启动的功能
  • 模型 -包含模型的名称,在我们的情况下,它只是一个用户界面
  • 服务 -包含...服务!
  • config.ts-应用程序配置文件
  • index.ts是我们应用程序的入口点
  • routing.ts-包含API路由

框架选择


Node.js有很多很棒的框架。最受欢迎的一种是Express还有Express'a- Koa的现代版本不幸的是,Deno不支持Node.js库,选择的范围要小得多,但是有一个基于Koa- Oak的 Deno框架我们将其用于我们的小型项目。如果您从未使用过Koa,请放心,它看起来很像Express。

创建一个应用程序入口点


索引

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芯片之一-直接从Internet导入模块。之后,没有什么异常:我们创建一个应用程序,添加中间件,路由,最后启动服务器。一切与使用Express或Koa时相同。

创建配置


配置文件
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类似

添加用户模型


模型/user.ts

export interface User {
  id: string;
  name: string;
  role: string;
  jiraAdmin: boolean;
  added: Date;
}

创建路由文件


routing.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应用程序中复制了代码,对吗?

路线的事件处理


处理程序/ getUsers.ts

import { getUsers } from "../services/users.ts";

export default async ({ response }) => {
  response.body = await getUsers();
};

返回所有用户。如果您从未使用过Koa,那么我会解释。响应对象类似于Express中的resExpress中res对象具有几种方法,例如jsonsend,用于发送响应。在Oak和Koa中,我们需要设置要返回到response.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" };
};

增加服务


在创建适用于用户数据的服务之前,我们需要制作两个小型辅助服务。

服务/ 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文件之前必须将其转换为String类型

最后是用于处理用户数据的主要服务。

服务/users.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);
};

这里有很多代码,但这是纯Typescript。

错误处理


与在处理用户数据时出现服务错误的情况相比,还有什么比这更糟的呢?整个程序可能会崩溃。为了避免这种情况,可以在每个处理程序中使用try / catch构造。但是,还有一个更优雅的解决方案-在每个路由的前面添加中间件,并防止可能发生的任何意外错误。

中间件/ error.ts

export default async ({ response }, next) => {
  try {
    await next();
  } catch (err) {
    response.status = 500;
    response.body = { msg: err.message };
  }
};

在启动程序本身之前,我们需要添加数据以运行。

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"
  }
]

就这样!现在您可以尝试运行我们的应用程序:

deno -A index.ts

“ A”标志意味着该程序无需获得任何单独的权限。不要忘记使用此标志在生产中是不安全的。

最有可能的是,您会看到许多行包含下载(下载)和编译(编译)行。最后,令人垂涎的行应该出现:

Listening on 4000

总结一下


我们使用了哪些工具?

  1. Deno用于读取/写入文件的全局对象
  2. 来自Deno标准库的uuid创建唯一的ID
  3. oak-受Koa启发用于Node.js的第三方框架
  4. Javascript中包含的纯Typescript,TextEncode或JSON对象

与Node.js有什么区别?


  • 无需为Typescript或ts-node等其他工具安装和配置编译器。您可以使用deno index.ts命令简单地运行该程序
  • 直接在代码中包含所有第三方模块,而无需进行初步安装
  • 缺少package.json和package-lock.json
  • 我们程序的根目录中没有node_modules。所有下载的文件都位于全局缓存中

如有必要,可在此处找到源代码

Source: https://habr.com/ru/post/undefined/


All Articles