
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 може зробити більше. Не краще: що насправді кажуть дослідження про розробку ігор
Generative AI входить у production ігор, але докази складніші за хайп. Розбираємо adoption, ставлення гравців, ризики якості та практичний workflow.

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 допомагає брендам перемагати у «агентному» вебі.
Професійна розробка для вашого бізнесу
Створюємо сучасні веб-рішення та боти для бізнесу. Дізнайтеся, як ми можемо допомогти вам досягти цілей.