- Drizzle ORM = TypeScript first, schema-as-code, query SQL-like, không magic như Prisma.
- Migration đơn giản: drizzle-kit generate + drizzle-kit migrate, không cần ORM service riêng.
- Performance hơn Prisma 30-50% vì không cần spawn engine process Rust.
- Cài Postgres trên VPS 2GB, kết nối qua DATABASE_URL, build app trong 30 phút.
- Phù hợp prototype SaaS, indie hacker dev solo, side project muốn deploy nhanh.
Bạn đang code prototype SaaS bằng Next.js hoặc Hono trên VPS, muốn chọn ORM cho Postgres mà không bị Prisma kéo theo 200MB binary + Rust engine? Drizzle ORM là câu trả lời. Stack Drizzle + Postgres tự host trên Cloud VPS 2GB cho phép bạn ship MVP trong vài ngày, query type-safe, schema as code.
Drizzle khác Prisma ở triết lý: nó không generate client từ schema rồi gọi qua RPC. Drizzle là SQL builder kèm type inference, query chạy thẳng qua node-postgres driver. Không có engine process, không có generation step nặng, deploy ra Cloud Run hay edge runtime đều được.
Bài này hướng dẫn full flow: cài Postgres trên VPS, setup Drizzle trong Next.js project, viết schema, generate migration, query mẫu, và deploy production. Test trên Cloud VPS TND 2GB Ubuntu 24.04, tổng thời gian setup 30-45 phút.
1. Vì sao chọn Drizzle thay Prisma cho prototype?
| Tiêu chí | Drizzle | Prisma |
|---|---|---|
| Bundle size | ~50KB gzipped | ~200MB binary engine |
| Cold start | ~50ms | ~500-1000ms (spawn engine) |
| Query syntax | SQL-like, gần native | Magic chain methods |
| Edge runtime | Hỗ trợ đầy đủ | Cần Accelerate proxy |
| Migration | SQL file thuần | Custom format |
| Type inference | Tự động từ schema | Cần prisma generate |
| Learning curve | Biết SQL là đủ | Cần học DSL riêng |
Drizzle không tốt hơn Prisma trong mọi case. Prisma có UI Prisma Studio, ecosystem lớn, nhiều plugin. Nhưng cho prototype tốc độ cao, Drizzle thắng tuyệt đối về performance và simplicity.
2. Cài Postgres 16 trên VPS 2GB
sudo apt update
sudo apt install -y postgresql-16
sudo systemctl enable --now postgresql
# Tạo user và db
sudo -u postgres psql
CREATE USER drizzle_user WITH PASSWORD 'random_strong_password';
CREATE DATABASE drizzle_app OWNER drizzle_user;
GRANT ALL PRIVILEGES ON DATABASE drizzle_app TO drizzle_user;
q
# Mở port 5432 cho local only (an toàn)
sudo nano /etc/postgresql/16/main/postgresql.conf
# listen_addresses = 'localhost'
sudo systemctl restart postgresqlTest connection từ shell:
psql postgres://drizzle_user:[email protected]:5432/drizzle_app
# drizzle_app=> dt
# (no tables)3. Khởi tạo Next.js + Drizzle project
npx create-next-app@latest my-app --typescript --app --no-src-dir
cd my-app
npm install drizzle-orm postgres
npm install -D drizzle-kit @types/nodeTạo file .env.local:
DATABASE_URL=postgres://drizzle_user:[email protected]:5432/drizzle_appTạo file drizzle.config.ts ở root:
import type { Config } from 'drizzle-kit';
export default {
schema: './db/schema.ts',
out: './db/migrations',
dialect: 'postgresql',
dbCredentials: {
url: process.env.DATABASE_URL!,
},
} satisfies Config;4. Viết schema first table với Drizzle
Tạo file db/schema.ts - đây là source of truth cho cả database structure và TypeScript types:
import { pgTable, serial, text, timestamp, integer, boolean } from 'drizzle-orm/pg-core';
export const users = pgTable('users', {
id: serial('id').primaryKey(),
email: text('email').notNull().unique(),
name: text('name'),
passwordHash: text('password_hash').notNull(),
isActive: boolean('is_active').default(true).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
});
export const posts = pgTable('posts', {
id: serial('id').primaryKey(),
userId: integer('user_id').notNull().references(() => users.id),
title: text('title').notNull(),
slug: text('slug').notNull().unique(),
content: text('content'),
published: boolean('published').default(false).notNull(),
createdAt: timestamp('created_at').defaultNow().notNull(),
updatedAt: timestamp('updated_at').defaultNow().notNull(),
});
export type User = typeof users.$inferSelect;
export type NewUser = typeof users.$inferInsert;
export type Post = typeof posts.$inferSelect;
export type NewPost = typeof posts.$inferInsert;5. Generate và apply migration
# Thêm script vào package.json
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
npm run db:generate
# Tạo file db/migrations/0000_xxxxx.sql với CREATE TABLE
npm run db:migrate
# Apply migration vào PostgresFile migration là SQL thuần, bạn có thể edit tay nếu cần tùy chỉnh trước khi apply. Khác với Prisma, không có hidden state.
# Xem cấu trúc DB
npm run db:studio
# Mở https://local.drizzle.studio - UI quản lý DB6. Setup Drizzle client cho query
// db/client.ts
import { drizzle } from 'drizzle-orm/postgres-js';
import postgres from 'postgres';
import * as schema from './schema';
const connectionString = process.env.DATABASE_URL!;
const client = postgres(connectionString, {
max: 10, // connection pool size
idle_timeout: 20,
connect_timeout: 10,
});
export const db = drizzle(client, { schema });Lưu ý: ở Next.js edge runtime, dùng @vercel/postgres hoặc neon-serverless thay postgres package (HTTP-based, không cần TCP).
7. CRUD query đầu tiên
// app/api/users/route.ts
import { db } from '@/db/client';
import { users } from '@/db/schema';
import { eq } from 'drizzle-orm';
import { NextResponse } from 'next/server';
// GET /api/users
export async function GET() {
const allUsers = await db.select().from(users);
return NextResponse.json(allUsers);
}
// POST /api/users
export async function POST(req: Request) {
const body = await req.json();
const [newUser] = await db
.insert(users)
.values({
email: body.email,
name: body.name,
passwordHash: body.passwordHash,
})
.returning();
return NextResponse.json(newUser, { status: 201 });
}Đẹp ở chỗ: newUser tự động được infer type User từ schema. Không cần generate, không cần annotation tay.
8. Join, where, order - query phức tạp hơn
import { db } from '@/db/client';
import { users, posts } from '@/db/schema';
import { eq, desc, and, like } from 'drizzle-orm';
// Lấy tất cả post của user kèm thông tin user
const result = await db
.select({
postId: posts.id,
title: posts.title,
authorName: users.name,
authorEmail: users.email,
})
.from(posts)
.innerJoin(users, eq(posts.userId, users.id))
.where(and(
eq(posts.published, true),
like(posts.title, '%nextjs%')
))
.orderBy(desc(posts.createdAt))
.limit(20);
// Drizzle generate SQL gần như native, dễ debug
console.log(result);Khác Prisma findMany({ where: { posts: { some: ... } } }) magic, Drizzle viết gần SQL thuần - dev biết SQL học trong 10 phút.
9. Relations và query builder advanced
Drizzle có 2 cách query: select chain (như SQL) và relational query (như Prisma). Cả 2 đều type-safe.
// db/schema.ts - thêm relations
import { relations } from 'drizzle-orm';
export const usersRelations = relations(users, ({ many }) => ({
posts: many(posts),
}));
export const postsRelations = relations(posts, ({ one }) => ({
author: one(users, {
fields: [posts.userId],
references: [users.id],
}),
}));
// Query với relational API
const userWithPosts = await db.query.users.findFirst({
where: eq(users.id, 1),
with: {
posts: {
orderBy: [desc(posts.createdAt)],
limit: 5,
},
},
});10. Transaction và batch insert
// Transaction
const result = await db.transaction(async (tx) => {
const [user] = await tx.insert(users).values({
email: '[email protected]',
passwordHash: 'hashed',
}).returning();
await tx.insert(posts).values({
userId: user.id,
title: 'First post',
slug: 'first-post',
});
return user;
});
// Batch insert nhiều rows
await db.insert(posts).values([
{ userId: 1, title: 'Post 1', slug: 'p1' },
{ userId: 1, title: 'Post 2', slug: 'p2' },
{ userId: 2, title: 'Post 3', slug: 'p3' },
]);11. Deploy lên VPS production
# Build trên VPS
cd /var/app
git pull origin main
npm ci --production=false
npm run db:migrate # apply pending migrations
npm run build
NODE_ENV=production pm2 restart web
# Hoặc systemd service
sudo systemctl restart app.serviceDrizzle không cần generate client trước build, không có postinstall hook nặng. Deploy nhanh hơn Prisma 2-3x.
12. So sánh performance với Prisma
| Metric | Drizzle | Prisma | Chênh lệch |
|---|---|---|---|
| Cold start API route | 80ms | 650ms | Drizzle nhanh 8x |
| Simple SELECT 1 row | 3ms | 8ms | Drizzle nhanh 2.5x |
| JOIN 3 table | 12ms | 22ms | Drizzle nhanh 1.8x |
| RAM dùng (Next.js prod) | 180MB | 320MB | Drizzle ít hơn 140MB |
| Bundle size client | 50KB | 0 (server only) | tương đương |
Test trên Cloud VPS TND 2GB, dataset 10000 rows, lặp 100 lần lấy trung bình.
13. Limitations và khi nào KHÔNG nên dùng Drizzle
- Cần Prisma Studio UI đẹp - Drizzle Studio ổn nhưng ít tính năng hơn.
- Team đã quen Prisma 2+ năm - chi phí switch cao.
- Cần plugin ecosystem rộng như nestjs-prisma, prisma-trpc-generator.
- Cần support cho 10+ database (Drizzle hỗ trợ Postgres, MySQL, SQLite tốt nhất).
Stack Drizzle + Postgres VPS 2GB - prototype trong 1 ngày
Cloud VPS TND sẵn AlmaLinux 9, Ubuntu 22/24, Debian 12/13. SSD CEPH, snapshot 1-click, backup hằng ngày, network 200Mbps trong nước. Cài Postgres + Node.js trong 5 phút, deploy Next.js + Drizzle ORM ngay.
Xem 8 cấu hình Cloud VPS →FAQ
Drizzle có thay được Prisma hoàn toàn không?
Cho prototype và project mới, có. Drizzle thiếu vài tính năng nhỏ như Prisma Migrate dev (resetable migration). Nhưng query, type-safety, performance đều tốt hơn Prisma trên VPS 2GB.
Drizzle có chạy được trên edge runtime Vercel hoặc Cloudflare Workers không?
Có. Drizzle hỗ trợ neon-serverless cho Neon, @vercel/postgres cho Vercel, và Cloudflare D1. Bundle nhỏ 50KB, deploy edge thoải mái.
Migration của Drizzle có rollback được không?
Drizzle không có auto rollback như Prisma. Cách backup: dùng pg_dump trước migration. Hoặc viết down migration tay nếu cần. Cho prototype thì khôi phục từ snapshot Cloud VPS nhanh hơn.
Drizzle Studio có thay được pgAdmin không?
Cho task xem/edit data đơn giản thì có. Drizzle Studio UI đẹp, type-aware. pgAdmin mạnh hơn cho DBA task như EXPLAIN, partition, replication.
Schema Drizzle có support enum và JSON column không?
Có. pgEnum cho enum, jsonb cho JSON column với type inference từ TypeScript interface. Cú pháp: const status = pgEnum(status, [active, inactive]).
Có nên migrate từ Prisma sang Drizzle giữa chừng project không?
Chỉ migrate khi gặp pain point thực tế: cold start chậm, RAM cao trên VPS nhỏ, edge runtime không chạy. Nếu Prisma đang ổn thì không cần switch chỉ vì hype.
- Playwright + Crawlee + VPS scrape 10k page mỗi ngày không bị ban
- codex --dangerously-bypass-approvals-and-sandbox
- Tại sao chọn IIS 7.5 ?
- OpenClaw Voice Mode: nói chuyện với agent qua iPhone/Android (setup + wake word)
- Setup Wiki.js cho dev team trên VPS: docs nội bộ ngon
- OpenClaw Live Canvas: dùng AI agent vẽ + render trực quan workspace của bạn



