
Bun.js + Elysia + Drizzle: типобезпечний REST API з PostgreSQL
Практичний приклад API на Bun.js: Elysia для маршрутів, Drizzle ORM для типобезпечних SQL-запитів, PostgreSQL для даних і Zod для валідації.
Elysia обробляє HTTP-маршрути, Drizzle описує таблиці TypeScript-кодом, а PostgreSQL зберігає дані. Bun запускає все одним процесом і не потребує окремого transpile-кроку.
Чому саме цей стек
Кожна бібліотека має одну чітку роль, тому код легко замінювати або тестувати окремо.
Elysia
Bun-native HTTP-фреймворк із зручними route-маршрутами, middleware та схемами.
Drizzle
SQL-first ORM: таблиці описуються TypeScript-кодом, а запити залишаються близькими до SQL.
PostgreSQL
Надійне сховище для production, індексів, транзакцій і майбутнього росту домену.
Zod
Явно перевіряє вхідні дані й не дозволяє помилковому payload дійти до сервісного шару.
Запит проходить валідацію до SQL-рівня, а відповідь повертається клієнту з типізованим контрактом.
Скріншот секції stackКрок 1: створюємо проєкт і підключаємо залежності
Bun сам встановить пакети й запускатиме TypeScript-файли без окремого bundler-а.
Ініціалізація
mkdir tasks-api && cd tasks-api
bun init
bun add elysia drizzle-orm postgres zod
bun add -d drizzle-kitЗмінні середовища
DATABASE_URL=postgres://app:app@localhost:5432/tasks
PORT=3000Bun автоматично читає .env під час запуску. Секрети не потрібно вбудовувати у вихідний код.
Конфігурація міграцій
// drizzle.config.ts
import { defineConfig } from "drizzle-kit";
export default defineConfig({
schema: "./src/db/schema.ts",
out: "./drizzle",
dialect: "postgresql",
dbCredentials: { url: process.env.DATABASE_URL! },
});Додайте scripts: "db:generate": "drizzle-kit generate" та "db:migrate": "drizzle-kit migrate".
Крок 2: описуємо таблицю та міграцію
Таблиця є джерелом типів для insert і select. Не створюйте паралельний ручний тип Task, якщо його можна вивести з Drizzle.
// src/db/schema.ts
import { boolean, pgTable, text, timestamp, uuid } from "drizzle-orm/pg-core";
export const tasks = pgTable("tasks", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
done: boolean("done").notNull().default(false),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow().notNull(),
});
export type Task = typeof tasks.$inferSelect;
export type NewTask = typeof tasks.$inferInsert;Запустіть bun run db:generate, а потім bun run db:migrate. Міграцію комітьте в репозиторій: production має застосовувати відомий SQL, а не генерувати його навмання.
Крок 3: додаємо типобезпечні маршрути Elysia
Elysia може перевіряти схеми на вході. Для невеликого прикладу використаємо Zod через t, але в реальному сервісі варто винести handler-и в окремий service layer.
// src/index.ts
import { Elysia, t } from "elysia";
import { desc, eq } from "drizzle-orm";
import { z } from "zod";
import { db } from "./db/client";
import { tasks } from "./db/schema";
const createTask = z.object({ title: z.string().trim().min(1).max(120) });
const app = new Elysia()
.get("/health", () => ({ status: "ok", runtime: "bun" }))
.get("/tasks", async () =>
db.select().from(tasks).orderBy(desc(tasks.createdAt)),
)
.post("/tasks", async ({ body, set }) => {
const input = createTask.parse(body);
const [task] = await db.insert(tasks).values(input).returning();
set.status = 201;
return task;
}, { body: t.Object({ title: t.String({ minLength: 1, maxLength: 120 }) }) })
.patch("/tasks/:id", async ({ params, body }) => {
const [task] = await db
.update(tasks)
.set({ done: body.done })
.where(eq(tasks.id, params.id))
.returning();
return task ?? new Response("Not found", { status: 404 });
}, {
params: t.Object({ id: t.String() }),
body: t.Object({ done: t.Boolean() }),
})
.listen(Number(Bun.env.PORT ?? 3000));
console.log(`API running at http://localhost:${app.server?.port}`);Повний database client і Docker Compose
З'єднання створюємо один раз на процес. Для локальної розробки Compose дає команді однакову версію PostgreSQL, а healthcheck не дозволяє міграціям стартувати раніше за базу.
// src/db/client.ts
import postgres from "postgres";
import { drizzle } from "drizzle-orm/postgres-js";
const sql = postgres(Bun.env.DATABASE_URL!, {
max: Number(Bun.env.DB_POOL_SIZE ?? 10),
prepare: false,
});
export const db = drizzle(sql);Для serverless-провайдера обирайте pooler або HTTP-драйвер, а для довгоживучого Bun-сервера звичайний pool із лімітом з'єднань зазвичай простіший.
# compose.yaml
services:
postgres:
image: postgres:16-alpine
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: tasks
ports: ["5432:5432"]
healthcheck:
test: ["CMD-SHELL", "pg_isready -U app -d tasks"]
interval: 2s
timeout: 3s
retries: 10Транзакції, помилки та production-поради
Швидкий runtime не компенсує нечіткі межі даних. Ось три правила, які збережуть API передбачуваним.
Тестуємо handler без магії
Elysia дозволяє викликати застосунок через handle, тому smoke-тест не потребує відкритого порту. Для інтеграційного тесту підставте тестову базу через DATABASE_URL.
// src/index.test.ts
import { describe, expect, test } from "bun:test";
import { app } from "./index";
describe("tasks API", () => {
test("rejects an empty title", async () => {
const response = await app.handle(
new Request("http://localhost/tasks", {
method: "POST",
headers: { "content-type": "application/json" },
body: JSON.stringify({ title: " " }),
}),
);
expect(response.status).toBe(422);
});
});Експортуйте app окремо від listen, щоб тест не запускав справжній listener. Запуск: bun test.
Перевірка локально та в CI
Запустіть PostgreSQL та API
docker run --name tasks-db -e POSTGRES_PASSWORD=app -e POSTGRES_USER=app -e POSTGRES_DB=tasks -p 5432:5432 -d postgres:16
bun run db:migrate
bun run src/index.tsСтворіть задачу
curl -X POST http://localhost:3000/tasks -H "content-type: application/json" -d '{"title":"Перевірити Bun API"}'
curl http://localhost:3000/tasksЗалиште короткий pipeline
bun install --frozen-lockfile
bun run db:migrate
bun testДля тестів піднімайте окрему базу або використовуйте Testcontainers; не запускайте тести проти production PostgreSQL.
Часті запитання
Так. Drizzle працює з Bun, а для PostgreSQL можна використовувати пакет `postgres` або сумісний драйвер. Перевіряйте версії драйвера у своєму deployment-середовищі.
Для запуску API на Bun — ні. Bun має власний runtime, package manager і TypeScript execution. Node.js може залишатися встановленим для інших проєктів.
Висновок
Опишіть задачу — перші 15 хвилин консультації безкоштовні.
Пов'язані статті

Скільки коштує розробка AI асистента у 2026: RAG чатбот, база знань, CRM, Telegram та підтримка
Практичний гід для бізнесу: від чого залежить ціна розробки AI асистента у 2026 році, що входить у RAG чатбот, інтеграції з CRM, Telegram, guardrails, оцінювання, моніторинг і супровід.

AI для розробки лендінгів: де він реально прискорює запуск, а де псує конверсію
Дослідження про використання AI у розробці лендінгів: v0, Webflow AI, Builder.io, Framer-подібні AI builders, генерація UX, copy, SEO, персоналізація, A/B тести, ризики шаблонності, безпеки, доступності та технічного боргу.

AI SEO / GEO у 2026: ваші наступні клієнти — не люди, а агенти
Пошук зміщується від кліків до відповідей. Боти та AI-агенти сканують, цитують, рекомендують і дедалі частіше купують. Дізнайтесь, що таке AI SEO / GEO, чому класичного SEO вже недостатньо, і як PAS7 Studio допомагає брендам перемагати у «агентному» вебі.

Найпотужніший чіп від Apple? M5 Pro і M5 Max б'ють рекорди
Аналітичний розбір Apple M5 Pro і M5 Max станом на березень 2026 року. Пояснюємо, чому ці чіпи можна вважати найпотужнішими професійними ноутбучними SoC від Apple, як вони виглядають на тлі M4 Pro, M4 Max, M1 Pro, M1 Max і що показують у порівнянні з актуальними Intel та AMD.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.