使用Bun建立應用程式API

使用Bun建立應用程式API

文章目录

  • 設定開發環境
  • 設定資料庫
  • 建立 Prisma 模型
  • 建立服務和控制器
  • 實現服務邏輯
  • 實現控制器邏輯
  • 設定 Bun-Elysia 伺服器
  • 小結

使用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。

評論留言