Bun 是与 Node 和 Deno 竞争的新 JavaScript Runtime,具有更快的速度和其他一些特性。本文将介绍如何使用 Bun 创建完整的 API。
Bun.js 是一种新的 JavaScript 运行环境,与 Node.js 和 Deno 相似,但速度更快、更独立。它采用快速的底层语言 Zig 编写,并利用了为 Safari 等 Webkit 浏览器提供支持的 JavaScriptCore Engine。Zig 与 JavaScriptCore 引擎的结合使 Bun 成为速度最快的 JavaScript 运行环境时之一。
此外,Bun 不仅仅是一个运行环境。它还是软件包管理器、测试运行程序和捆绑程序。在本教程中,您将学习如何使用 Elysia、PostgreSQL 和 Prisma 通过 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 键选择默认值,如下图所示。
当前目录应该如下图所示。
接下来,运行下面的命令安装所需的依赖项:
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 将涉及三个表:Recipes, Users, 和 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; };
上面的代码块导出了两个函数: verifyToken
和 signUserToken
。verifyToken
函数接收用户的访问令牌并检查其有效性。如果有效,它就会解码令牌并返回令牌中包含的信息,否则就会出错。
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; } };
上面的代码块导出了两个函数: createNewUser
和 login
。createNewUser
函数接受用户名、电子邮件和密码。它使用 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; };
上面的代码块导出了三个函数: createRecipe
、 getAllRecipies
和 getRecipeById
。
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; };
上面的代码块导出了两个函数: createComment
和 getAllCommentsForRecipe
。 createComment
为特定配方创建新的注释,而 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。
评论留言