在这篇文章中,我想讲述和展示使用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.tsexport interface User {
id: string;
name: string;
role: string;
jiraAdmin: boolean;
added: Date;
}
创建路由文件
routing.tsimport { 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.tsimport { getUsers } from "../services/users.ts";
export default async ({ response }) => {
response.body = await getUsers();
};
返回所有用户。如果您从未使用过Koa,那么我会解释。响应对象类似于Express中的res。Express中的res对象具有几种方法,例如json或send,用于发送响应。在Oak和Koa中,我们需要设置要返回到response.body属性的值。处理程序/ getUserDetails.tsimport { 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.tsimport { 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.tsimport { 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.tsimport { 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.tsexport default ({ response }) => {
response.status = 404;
response.body = { msg: "Not Found" };
};
增加服务
在创建适用于用户数据的服务之前,我们需要制作两个小型辅助服务。服务/ createId.tsimport { v4 as uuid } from "https://deno.land/std/uuid/mod.ts";
export default () => uuid.generate();
每个新用户将收到一个唯一的ID。我们将使用Deno标准库中的uuid模块生成一个随机数。服务/ db.tsimport { 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.tsimport { 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();
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.tsexport 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
总结一下
我们使用了哪些工具?- Deno用于读取/写入文件的全局对象
- 来自Deno标准库的uuid创建唯一的ID
- oak-受Koa启发用于Node.js的第三方框架
- Javascript中包含的纯Typescript,TextEncode或JSON对象
与Node.js有什么区别?
- 无需为Typescript或ts-node等其他工具安装和配置编译器。您可以使用deno index.ts命令简单地运行该程序
- 直接在代码中包含所有第三方模块,而无需进行初步安装
- 缺少package.json和package-lock.json
- 我们程序的根目录中没有node_modules。所有下载的文件都位于全局缓存中
如有必要,可在此处找到源代码。