使用Bun创建应用程序API

使用Bun创建应用程序API

Bun 是与 Node 和 Deno 竞争的新 JavaScript Runtime,具有更快的速度和其他一些特性。本文将介绍如何使用 Bun 创建完整的 API。

Bun.js 是一种新的 JavaScript 运行环境,与 Node.jsDeno 相似,但速度更快、更独立。它采用快速的底层语言 Zig 编写,并利用了为 Safari 等 Webkit 浏览器提供支持的 JavaScriptCore Engine。Zig 与 JavaScriptCore 引擎的结合使 Bun 成为速度最快的 JavaScript 运行环境时之一。

此外,Bun 不仅仅是一个运行环境。它还是软件包管理器、测试运行程序和捆绑程序。在本教程中,您将学习如何使用 ElysiaPostgreSQLPrisma 通过 Bun 创建一个简单的配方共享 API。

设置开发环境

要使用 Bun,首先要在系统中安装它。运行以下命令将 Bun 安装到 macOS、Linux 或 Windows Subsystem for Linux (WSL)。

curl -fsSL https://bun.sh/install | bash

目前,Bun 仅有一个用于 Windows 的实验版本,仅支持运行环境。

安装 Bun 后,运行下面的命令创建并 cd 到项目目录:

mkdir recipe-sharing-api && cd recipe-sharing-api

接下来,运行下面的命令初始化一个新的 Bun 应用程序:

bun init

上述命令将提示您输入应用程序的软件包名称和入口点。您可以按 ENTER 键选择默认值,如下图所示。

应用程序的软件包名称和入口点

当前目录应该如下图所示。

API目录

接下来,运行下面的命令安装所需的依赖项:

bun add elysia @elysiajs/cookie prisma @prisma/client dotenv pg jsonwebtoken@8.5.1

运行安装相应类型:

bun add -d @types/jsonwebtoken

您安装的依赖项是:

  • Elysia:Elysia 是 Bun 的网络框架,可简化与 Bun 的协作,类似于 Express 对 Node.js 的作用。
  • Prisma:Prisma 是 JavaScript 和 TypeScript 的对象关系映射器(ORM)。
  • Dotenv:Dotenv 是一个 NPM 软件包,用于将 .env 文件中的环境变量加载到 process.env 中。
  • PG:PG 是 PostgreSQL 的本地驱动程序。
  • jsonwebtoken@8.5.1实现 JWT 标准(8.5.1 版)的软件包。

设置数据库

食谱共享 API 将涉及三个表:RecipesUsers, 和 Comments。用户可以创建和共享菜谱,查看他人的菜谱,并对菜谱发表评论。

运行以下命令在应用程序中使用 PostgreSQL 初始化 Prisma:

bunx prisma init --datasource-provider postgresql

上述命令会创建一个 .env 文件和一个 Prisma 文件夹。您将在 Prisma 文件夹中找到 schema.prisma 文件。该文件包含数据库连接的配置。

接下来,打开 .env 文件,将虚拟数据库连接 URI 替换为数据库的连接 URI。

创建 Prisma 模型

Prisma 模型代表数据库中的表。Prisma 模式中的每个模型都对应数据库中的一个表,定义其结构。

打开 schema.prisma 文件,添加以下代码块以创建 User 模型。

model User {
id        Int      @id @default(autoincrement())
email     String   @unique
name      String?
password  String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
recipies  Recipe[]
comments  Comment[]
}

上面的代码块代表 User 模型。它包含应用程序所需的所有用户信息,如电子邮件、姓名、密码、食谱和评论。

当新用户注册时,您将创建一个新的 User 模型实例,当用户尝试登录时,您将获取该实例,并将存储的信息与登录请求中发送的信息进行比较。

接下来,将下面的代码块添加到 schema.prisma 文件中,以创建 Recipe 模型:

model Recipe {
id        Int      @id @default(autoincrement())
title     String
body      String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user      User     @relation(fields: [userId], references: [id])
userId    Int
comments  Comment[]
}

上面的代码块表示 Recipe 模型。它包含应用程序所需的所有配方信息,如标题、正文和创建配方的用户信息。

当用户创建配方时,您将创建一个新的 Recipe 模型实例。

然后,将下面的代码块添加到 schema.prisma 文件中,以创建 Comment 模型:

model Comment {
id        Int      @id @default(autoincrement())
body      String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
user      User     @relation(fields: [userId], references: [id])
userId    Int
recipe   Recipe  @relation(fields: [recipeId], references: [id])
recipeId Int
}

上面的代码块代表了您的 Comment 模型。它包含应用程序所需的所有注释信息,包括正文、日期、创建注释的用户以及注释的配方。

当用户对配方发表评论时,就会创建一个新的 Comment 模型实例。

最后,运行下面的命令生成并运行迁移

bunx prisma migrate dev --name init

您可以将 init 替换为您选择的任何名称。

运行上述命令将确保 Prisma 模式与数据库模式同步。

创建服务和控制器

在项目中创建服务和控制器有助于组织代码,使其更易于维护。

运行以下命令,在项目根目录下创建 controllers 和 services 文件夹:

mkdir controllers && mkdir services

接下来,在 services 文件夹中创建以下文件:

  • user.service.ts: 该文件包含与用户注册和登录相关的所有逻辑。
  • recipe.service.ts: 该文件包含创建和查看食谱的所有逻辑。
  • comment.service.ts: 该文件包含对菜谱进行评论的所有逻辑。
  • auth.service.ts: 该文件包含验证用户的逻辑。

然后,在 controllers 文件夹中创建以下文件:

  • comments.controller.ts: 该文件包含评论的所有控制器逻辑。
  • recipe.controller.ts: 该文件包含配方控制器的所有逻辑。
  • user.controller.ts: 该文件包含用户身份验证的所有控制器逻辑。

实现服务逻辑

服务是功能或逻辑的独特单元,旨在执行特定任务。

要实现这一点,请打开 auth.service.ts 文件并添加以下代码块。

//auth.service.ts
import jwt from "jsonwebtoken";
export const verifyToken = (token: string) => {
let payload: any;
//Verify the JWT token
jwt.verify(token, process.env.JWT_SECRET as string, (error, decoded) => {
if (error) {
throw new Error("Invalid token");
}
payload = decoded;
});
return payload;
};
export const signUserToken = (data: { id: number; email: string }) => {
//Sign the JWT token
const token = jwt.sign(
{
id: data.id,
email: data.email,
},
process.env.JWT_SECRET as string,
{ expiresIn: "1d" }
);
return token;
};

上面的代码块导出了两个函数: verifyTokensignUserTokenverifyToken 函数接收用户的访问令牌并检查其有效性。如果有效,它就会解码令牌并返回令牌中包含的信息,否则就会出错。

signUserToken 函数将用户数据作为有效载荷,创建并返回有效期为一天的 JWT。

接下来,打开 user.service.ts 文件并添加下面的代码块:

//user.service.ts
import { prisma } from "../index";
import { signUserToken } from "./auth.service";
export const createNewUser = async (data: {
name: string;
email: string;
password: string;
}) => {
try {
const { name, email, password } = data;
//Hash the password using the Bun package and bcrypt algorithm
const hashedPassword = await Bun.password.hash(password, {
algorithm: "bcrypt",
});
//Create the user
const user = await prisma.user.create({
data: {
name,
email,
password: hashedPassword,
},
});
return user;
} catch (error) {
throw error;
}
};
export const login = async (data: { email: string; password: string }) => {
try {
const { email, password } = data;
//Find the user
const user = await prisma.user.findUnique({
where: {
email,
},
});
if (!user) {
throw new Error("User not found");
}
//Verify the password
const valid = await Bun.password.verify(password, user.password);
if (!valid) {
throw new Error("Invalid credentials");
}
//Sign the JWT token
const token = signUserToken({
id: user.id,
email: user.email,
});
return {
message: "User logged in successfully",
token,
};
} catch (error) {
throw error;
}
};

上面的代码块导出了两个函数: createNewUserlogincreateNewUser 函数接受用户名、电子邮件和密码。它使用 Bun 内置的密码模块对密码进行散列,并使用所提供的信息创建一个新用户。

login 函数接收用户的凭据,并与数据库中存储的记录进行验证。如果匹配,就会为用户创建一个访问令牌;否则就会出错。

接下来,打开 recipe.service.ts 文件,添加下面的代码块。

//recipe.service.ts
import { prisma } from "../index";
export const createRecipe = async (data: {
title: string;
body: string;
userId: number;
}) => {
const { title, body, userId } = data;
//Create the recipe
const recipe = await prisma.recipe.create({
data: {
title,
body,
userId,
},
});
return recipe;
};
export const getAllRecipes = async () => {
//Get all recipes
const recipes = await prisma.recipe.findMany({
include: {
user: true,
comments: true,
},
});
return recipes;
};
export const getRecipeById = async (id: number) => {
//Get recipe by id and include the user
const recipe = await prisma.recipe.findUnique({
where: {
id,
},
include: {
user: true,
},
});
return recipe;
};

上面的代码块导出了三个函数: createRecipegetAllRecipiesgetRecipeById

createRecipe 函数根据作为参数传递的数据创建新配方并返回。 getAllRecipies 函数检索并返回数据库中的所有菜谱。getRecipeById 函数根据作为参数传递的 id 获取配方并返回。

接下来,打开你的 comments.service.ts 文件并添加下面的代码块。

//comments.service.ts
import { prisma } from "../index";
export const createComment = async (data: {
body: string;
recipeId: number;
userId: number;
}) => {
try {
const { body, recipeId, userId } = data;
//Create the comment for the recipe with the given id
const comment = await prisma.comment.create({
data: {
body,
userId,
recipeId: recipeId,
},
});
return comment;
} catch (error: any) {
throw error;
}
};
export const getAllCommentsForRecipe = async (recipeId: number) => {
//Get all comments for the recipe with the given id
const comments = await prisma.comment.findMany({
where: {
recipeId,
},
include: {
user: true,
},
});
return comments;
};

上面的代码块导出了两个函数: createCommentgetAllCommentsForRecipecreateComment 为特定配方创建新的注释,而 getAllCommentsForRecipe 则返回特定配方的所有注释。

实现控制器逻辑

与使用 request 和 response 对象处理请求的 Express.js 不同,Elysia 使用上下文对象

上下文对象提供的方法与 Express 的 request 和 response 对象类似。此外,Elysia 还会自动将控制器函数的返回值映射到响应中,并将其返回给客户端。

要实现控制器逻辑,请打开 user.controller.ts 文件并添加以下代码块。

//user.controller.ts
import Elysia from "elysia";
import { createNewUser, login } from "../services/user.service";
export const userController = (app: Elysia) => {
app.post("/signup", async (context) => {
try {
const userData: any = context.body;
const newUser = await createNewUser({
name: userData.name,
email: userData.email,
password: userData.password,
});
return {
user: newUser,
};
} catch (error: any) {
return {
error: error.message,
};
}
});
app.post("/login", async (context) => {
try {
const userData: any = context.body;
const loggedInUser = await login({
email: userData.email,
password: userData.password,
});
return loggedInUser;
} catch (error: any) {
console.log(error);
return {
error: error.message,
};
}
});
};

上面的代码块实现了 /signup/login 的控制器逻辑。

当用户向 /signup 发送 POST 请求时,控制器将从上下文对象( context.body )中提取请求正文,并将其传递给在 users.service.ts 文件中创建的 createNewUser 函数。

当用户向 /login 发送 POST 请求时,控制器将从上下文对象中提取请求体,并将电子邮件和密码传递给登录函数。如果用户信息正确无误,控制器将返回一条成功消息和访问令牌。

接下来,打开 recipe.controller.ts 文件,添加下面的代码块。

//recipe.controller.ts
import Elysia from "elysia";
import { createRecipe, getAllRecipes } from "../services/recipe.service";
import { verifyToken } from "../services/auth.service";
export const recipeController = (app: Elysia) => {
app.post("/create-recipe", async (context) => {
try {
const authHeader = context.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
if (!token) {
throw new Error("Invalid token");
}
const verifiedToken = verifyToken(token as string);
const recipeData: any = context.body;
const newRecipe = await createRecipe({
title: recipeData.title,
body: recipeData.body,
userId: verifiedToken?.id,
});
return {
recipe: newRecipe,
};
} catch (error: any) {
return {
error: error.message,
};
}
});
app.get("/recipes", async () => {
try {
const recipes = await getAllRecipes();
return recipes;
} catch (error: any) {
return {
error: error.message,
};
}
});
};

上面的代码块实现了 /create-recipe/recipes 的控制器逻辑。

当用户向 /create-recipe 发出 POST 请求时,控制器将检查用户是否拥有有效的访问令牌(检查用户是否已登录)。

如果用户没有访问令牌或令牌无效,控制器就会出错。如果令牌有效,控制器将从上下文对象中提取配方详细信息,并将其传递给 createRecipe 函数。

当用户向 /recipes 发送 GET 请求时,控制器将调用 getAllRecipes 函数并返回所有菜谱。

接下来,打开您的 comments.controller.ts,添加下面的代码块。

//comments.controller.ts
import Elysia from "elysia";
import {
createComment,
getAllCommentsForRecipe,
} from "../services/comments.service";
import { verifyToken } from "../services/auth.service";
export const commentController = (app: Elysia) => {
app.post("/:recipeId/create-comment", async (context) => {
try {
const authHeader = context.headers["authorization"];
const token = authHeader && authHeader.split(" ")[1];
const recipeId = context.params.recipeId;
if (!token) {
throw new Error("Invalid token");
}
const verifiedToken = verifyToken(token as string);
const commentData: any = context.body;
const newComment = await createComment({
body: commentData.body,
recipeId: +recipeId,
userId: verifiedToken?.id,
});
return newComment;
} catch (error: any) {
return {
error: error.message,
};
}
});
app.get("/:recipeId/comments", async (context) => {
try {
const recipeId = context.params.recipeId;
const comments = await getAllCommentsForRecipe(+recipeId);
return {
comments,
};
} catch (error: any) {
return {
error: error.message,
};
}
});
};

上面的代码块实现了 /:recipeId/create-comment/:recipeId/comments 的控制器逻辑。

当用户向 /:recipeId/create-comment 发出POST请求时,控制器会检查用户是否已登录,如果已登录,就会从上下文对象中提取评论详情,并将其传递给 createComment 函数。

当用户向 /:recipeId/comments 发送 GET 请求时,控制器会从上下文对象( context.params.recipeId )中提取 recipeId,并将其作为参数传递给 getAllCommentsForRecipe,然后使用显式类型强制将其转换为数字。

设置 Bun-Elysia 服务器

创建服务和控制器后,您必须设置一个服务器来处理传入的请求。

要创建 Bun-Elysia 服务器,请打开 index.ts 文件并添加以下代码块。

//index.ts
import Elysia from "elysia";
import { recipeController } from "./controllers/recipe.controller";
import { PrismaClient } from "@prisma/client";
import { userController } from "./controllers/user.controller";
import { commentController } from "./controllers/comments.controller";
//Create instances of prisma and Elysia
const prisma = new PrismaClient();
const app = new Elysia();
//Use controllers as middleware
app.use(userController as any);
app.use(recipeController as any);
app.use(commentController as any);
//Listen for traffic
app.listen(4040, () => {
console.log("Server is running on port 4040");
});
export { app, prisma };

上面的代码块导入了所有控制器、Elysia 框架和 PrismaClient。它还创建了 Prisma 和 Elysia 实例,并将控制器注册为中间件,以便将所有传入请求路由到正确的处理程序。

然后,它会监听 4040 端口的传入流量,并将 Elysia 和 Prisma 实例导出到应用程序的其他部分。

最后,运行下面的命令启动应用程序:

bun --watch index.ts

上述命令以观察模式启动 Bun 应用程序。

小结

在本文中,你将学习如何使用 Bun、Elysia、Prisma 和 Postgres 构建一个简单的 API。您已经学会了安装和配置 Bun、构建数据库,以及实施模块化服务和控制器以实现高效的代码管理。您可以使用任何 API 测试工具(如 Postman 或 Insomnia)测试您构建的 API。

评论留言