Ваш первый API с Bun, Express и Prisma
Вы уже увидели новую, классную и быструю среду разработки для JavaScript и задаетесь вопросом, как начать разрабатывать веб-приложения? Возможно, эта статья поможет вам. Мне нравится видеть новые способы создания приложений, которые привносят инновации в экосистему JS, а Bun привносит в нее нечто большее. Здесь, без дополнительных библиотек, вы можете создать свой API, протестировать его, собрать в пакет и даже использовать собственную интеграцию SQLite, и все это в быстрой и простой в использовании среде выполнения. В ней даже уже есть некоторые фреймворки, но это - наработки на будущее.
Установка и Hello World
Прежде всего, загрузите и установите bun
с помощью curl
, как указано в файле bun.sh
☁ ~ curl -fsSL 'https://bun.sh/install' | bash
######################################################################## 100,0%
bun was installed successfully to ~/.bun/bin/bun
Run 'bun --help' to get started
Затем создайте папку, в которой будет находиться ваш проект, перейдите в нее по cd
и выполните команду bun init
, в результате чего будет создан новый проект, в котором вам нужно будет выбрать имя проекта и точку входа. По умолчанию cli
будет использовать имя вашей папки и стартовать с index.ts
.
☁ projects mkdir bunApp
☁ projects cd bunApp
☁ bunApp bun init
bun init helps you get started with a minimal project and tries to guess sensible defaults. Press ^C anytime to quit
package name (bunapp):
entry point (index.ts):
Done! A package.json file was saved in the current directory.
+ index.ts
+ .gitignore
+ tsconfig.json (for editor auto-complete)
+ README.md
To get started, run:
bun run index.ts
☁ bunApp
После этого откройте ваш любимый ide (здесь я использую vscode), и вы увидите очень скромное содержимое, с некоторыми конфигурационными файлами и index.ts
, содержащим наш Hello World!!!
Почти все эти файлы являются общими для всех репозиториев, но есть один, который называется bun.lockb
, это автоматически генерируемый файл, похожий на другие .lock
-файлы, и сейчас он не так важен, но вы можете узнать о нем в документации по Bun.
Мы уже можем запустить наш файл index.ts
, чтобы начать наш маленький проект:
☁ bunApp bun index.ts
Hello via Bun!
☁ bunApp
Прежде чем мы перейдем к следующей теме, необходимо сделать еще одну вещь. Если вы знакомы с Node, то наверняка использовали Nodemon для мониторинга проекта и перезагрузки при изменении кода. Bun просто использует тег --watch
для работы в этом режиме, поэтому вам не нужен внешний модуль.
Давайте добавим в наш package.json два скрипта, один для запуска проекта, другой для режима разработчика, включая тег --watch
.
{
"name": "bunapp",
"module": "index.ts",
"type": "module",
"scripts": {
"start": "bun run index.ts",
"dev": "bun --watch run index.ts"
},
"devDependencies": {
"bun-types": "latest"
},
"peerDependencies": {
"typescript": "^5.0.0"
},
}
Маршруты
Для инициализации нашего сервера мы просто используем Bun.serve()
. Он может получать некоторые параметры, но сейчас нам нужен только порт для доступа к нашему приложению и обработчик fetch()
, который мы используем для обработки наших запросов. Напишите приведенный ниже код и запустите наш скрипт bun run dev
.
const server = Bun.serve({
port: 8080,
fetch(req) {
return new Response("Bun!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)
Это наш первый http-запрос. Так как мы не указали метод, то на любой запрос к нашей конечной точке, которая должна быть localhost:8080
, он вернет ответ Bun!
С этим мы разберемся в следующей теме, а пока просто добавим еще немного кода, следуя примеру из документации, для составления наших маршрутов.
const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") return new Response("Blog!")
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)
Он берет наш url из объекта запроса и разбирает его до объекта URL, используя API Node. Это происходит потому, что Bun стремится к полной совместимости с Node, поэтому большинство библиотек и пакетов, используемых на Node, работает на Bun изначально.
HTTP-запросы
Если хотите, используйте console.log(req)
для просмотра объекта нашего запроса, он выглядит следующим образом:
Listening on localhost: 8080...
Request (0 KB) {
method: "GET",
url: "http://localhost:8080/",
headers: Headers {
"host": "localhost:8080",
"connection": "keep-alive",
"upgrade-insecure-requests": "1",
"user-agent": "Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/117.0.0.0 Safari/537.36",
"accept": "text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8",
"accept-language": "en-US,en",
"sec-fetch-mode": "navigate",
"sec-fetch-dest": "document",
"accept-encoding": "gzip, deflate, br",
"if-none-match": "W/\"b-f4FzwVt2eK0ePdTZJcUnF/0T+Zw\"",
"sec-ch-ua": "\"Brave\";v=\"117\", \"Not;A=Brand\";v=\"8\", \"Chromium\";v=\"117\"",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": "\"Linux\"",
"sec-gpc": "1",
"sec-fetch-site": "none",
"sec-fetch-user": "?1"
}
}
Дело в том, что мы можем использовать множество условий для проверки метода и/или конечной точки. Это становится мучительно грязным и не очень приятным для чтения.
const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)const server = Bun.serve({
port: 8080,
fetch(req) {
const url = new URL(req.url)
if(url.pathname === "/") return new Response("Home Page!")
if(url.pathname === "/blog") {
switch (req.method) {
case "GET":
// handle with GET
case "POST":
// handle with POST
case "PUT":
// handle with PUT
case "DELETE":
// handle with DELETE
}
// any other routes and methods...
}
return new Response("404!")
}
})
console.log(`Listening on ${server.hostname}: ${server.port}...`)
Помните, что большинство пакетов Node работает и на Bun? Давайте воспользуемся Express для облегчения процесса разработки, подробности можно посмотреть в документации по Bun. Начнем с того, что остановим наше приложение с помощью CTRL + C
и выполним команду bun add express
.
Listening on localhost: 8080...
^C
☁ bunApp bun add express
bun add v1.0.2 (37edd5a6)
installed express@4.18.2
58 packages installed [1200.00ms]
☁ bunApp
Также перепишем наш index.ts, используя шаблон Express с некоторыми маршрутами.
import express, { Request, Response } from "express";
const app = express();
const port = 8080;
app.use(express.json());
app.post("/blog", (req: Request, res: Response) => {
//create new blog post
});
app.get("/", (req: Request, res: Response) => {
res.send("Api running");
});
app.get("/blog", (req: Request, res: Response) => {
//get all posts
});
app.get("/blog/:post", (req: Request, res: Response) => {
//get a specific post
});
app.delete("/blog/:post", (req: Request, res: Response) => {
//delete a post
});
app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
Добавление базы данных
И последнее, что необходимо сделать для нашего CRUD, - это реализовать соединения с базой данных. В Bun уже есть собственный драйвер SQLite3, но мы используем Prisma, так как работать с ORM проще. Давайте последуем указаниям из документации Bun и начнем с добавления Prisma с помощью bun add prisma
и инициализации ее с помощью bunx prisma init --datasource-provider sqlite
. Затем перейдем к нашему новому файлу schema.prisma
и вставим новую модель.
☁ bunApp bun add prisma
bun add v1.0.2 (37edd5a6)
installed prisma@5.3.1 with binaries:
- prisma
2 packages installed [113.00ms]
☁ bunApp bunx prisma init --datasource-provider sqlite
✔ Your Prisma schema was created at prisma/schema.prisma
You can now open it in your favorite editor.
warn You already have a .gitignore file. Don't forget to add `.env` in it to not commit any private information.
Next steps:
1. Set the DATABASE_URL in the .env file to point to your existing database. If your database has no tables yet, read https://pris.ly/d/getting-started
2. Run prisma db pull to turn your database schema into a Prisma schema.
3. Run prisma generate to generate the Prisma Client. You can then start querying your database.
More information in our documentation:
https://pris.ly/d/getting-started
☁ bunApp
После этого выполните команду bunx prisma generate
, а затем bunx prisma migrate dev --name init
. Теперь у нас есть все необходимое для нашего маленького API. Вернитесь к нашему index.ts
, импортируйте и инициализируйте клиент Prisma, после чего мы готовы завершить работу над маршрутами.
import { PrismaClient } from "@prisma/client";
/* Config database */
const prisma = new PrismaClient();
Итоговый файл index.ts
в конечном итоге должен выглядеть следующим образом:
import express, { Request, Response } from "express";
import { PrismaClient } from "@prisma/client";
/* Config database */
const prisma = new PrismaClient();
/* Config server */
const app = express();
const port = 8080;
app.use(express.json());
app.post("/blog", async (req: Request, res: Response) => {
try {
const { title, content } = req.body;
await prisma.post.create({
data: {
title: title,
content: content,
},
});
res.status(201).json({ message: `Post created!` });
} catch (error) {
console.error(`Something went wrong while create a new post: `, error);
}
});
app.get("/", (req: Request, res: Response) => {
res.send("Api running");
});
app.get("/blog", async (req: Request, res: Response) => {
try {
const posts = await prisma.post.findMany();
res.json(posts);
} catch (error) {
console.error(`Something went wrong while fetching all posts: `, error);
}
});
app.get("/blog/:postId", async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.postId, 10);
const post = await prisma.post.findUnique({
where: {
id: postId,
},
});
if (!post) res.status(404).json({ message: "Post not found" });
res.json(post);
} catch (error) {
console.error(`Something went wrong while fetching the post: `, error);
}
});
app.delete("/blog/:postId", async (req: Request, res: Response) => {
try {
const postId = parseInt(req.params.postId, 10);
await prisma.post.delete({
where: {
id: postId,
},
});
res.send(`Post deleted!`);
} catch (error) {
return res.status(404).json({ message: "Post not found" });
}
});
app.listen(port, () => {
console.log(`Listening on port ${port}...`);
});
Заключение
Наконец-то наш API готов! Дальше можно реализовать множество других вещей, таких как: добавление новых моделей, создание фронтенда и, конечно же, написание тестов (попробуем сделать это дальше). Вы видите, что Bun обеспечивает определенное качество жизни и при этом совместим с большинством пакетов Node, облегчая жизнь нашим разработчикам.