ทำไม Drizzle ORM + Neon ถึงเป็นคู่หูที่ลงตัวสำหรับ Next.js 16
ถ้าคุณเคยสร้างแอป Full-Stack ด้วย Next.js มาก่อน น่าจะเคยปวดหัวกับเรื่องฐานข้อมูลกันบ้าง — เลือก ORM ตัวไหนดี? ใช้ database อะไร? ตั้งค่ายังไงให้ทำงานกับ Server Components ได้ลื่นๆ? แล้วพอขึ้น production บน Vercel ก็ต้องมานั่งจัดการ connection pooling อีก จริงๆ แล้วเรื่องนี้ทำให้เสียเวลามากกว่าที่ควรจะเป็นเยอะเลย
ในปี 2026 คำตอบที่ชัดเจนที่สุดคือ Drizzle ORM + Neon PostgreSQL ครับ
Drizzle ORM เป็น TypeScript ORM ที่เน้น SQL-first approach — ให้ความรู้สึกเหมือนเขียน SQL จริงๆ แต่ได้ type safety เต็มรูปแบบ Bundle size เล็กมากจนน่าตกใจ เหมาะกับ serverless environment อย่าง Vercel Functions สุดๆ
ส่วน Neon เป็น serverless PostgreSQL ที่ออกแบบมาสำหรับ cloud-native โดยเฉพาะ มี scale-to-zero, database branching และ serverless driver ที่ทำงานผ่าน HTTP/WebSocket ได้เลย (ไม่ต้องมานั่งจัดการ TCP connection ให้วุ่นวาย)
ในคู่มือนี้ เราจะสร้างแอป Task Manager แบบ Full-Stack CRUD ตั้งแต่เริ่มต้นจนถึง deploy บน Vercel โดยใช้:
- Next.js 16 — App Router, Server Components, Server Actions
- Drizzle ORM — สำหรับจัดการฐานข้อมูลแบบ type-safe
- Neon PostgreSQL — serverless database ที่ deploy ง่ายและประหยัด
- drizzle-zod — สร้าง Zod validation schema จาก Drizzle schema อัตโนมัติ
เอาล่ะ มาเริ่มกันเลย
1. สร้างโปรเจกต์และติดตั้ง Dependencies
เริ่มจากสร้างโปรเจกต์ Next.js 16 ก่อนเลยครับ:
npx create-next-app@latest task-manager --typescript --app
cd task-manager
จากนั้นติดตั้ง Drizzle ORM และ Neon serverless driver:
npm install drizzle-orm @neondatabase/serverless
npm install -D drizzle-kit
สำหรับ validation ให้ติดตั้ง drizzle-zod และ zod ด้วย:
npm install drizzle-zod zod
โครงสร้างโฟลเดอร์ที่แนะนำหน้าตาประมาณนี้:
task-manager/
├── app/
│ ├── page.tsx
│ ├── layout.tsx
│ └── tasks/
│ ├── page.tsx
│ └── [id]/
│ └── page.tsx
├── db/
│ ├── schema.ts # Drizzle schema
│ ├── index.ts # Database connection
│ └── migrations/ # Migration files
├── lib/
│ └── actions.ts # Server Actions
├── components/
│ ├── TaskForm.tsx
│ └── TaskList.tsx
├── drizzle.config.ts
└── .env.local
2. ตั้งค่า Neon PostgreSQL Database
ก่อนเขียนโค้ดอะไรทั้งนั้น ต้องสร้าง database ก่อน:
- ไปที่ neon.tech แล้วสมัครบัญชี (มี free tier ให้ใช้งาน ไม่ต้องใส่บัตรเครดิต)
- สร้าง Project ใหม่ เลือก Region ที่ใกล้กับ Vercel deployment ของคุณ
- เมื่อสร้างเสร็จ จะได้ connection string หน้าตาประมาณนี้:
postgresql://username:[email protected]/neondb?sslmode=require
สร้างไฟล์ .env.local แล้วใส่ connection string เข้าไป:
# .env.local
DATABASE_URL="postgresql://username:[email protected]/neondb?sslmode=require"
ข้อสำคัญ: Neon รองรับ PostgreSQL 14-18 แล้วในปี 2026 รวมถึงฟีเจอร์ใหม่อย่าง UUIDv7 และ Virtual Generated Columns ด้วย ถ้าต้องการใช้ฟีเจอร์ล่าสุด ให้เลือก PostgreSQL 18 ตอนสร้าง project ได้เลยครับ
3. กำหนด Database Schema ด้วย Drizzle
ส่วนตัวคิดว่าตรงนี้แหละที่เป็นหัวใจสำคัญของ Drizzle เลย — การกำหนด schema ด้วย TypeScript ซึ่งทำหน้าที่เป็นทั้ง database structure และ TypeScript types ไปพร้อมกัน:
// db/schema.ts
import { pgTable, text, timestamp, boolean, uuid } from "drizzle-orm/pg-core"
export const tasks = pgTable("tasks", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
description: text("description"),
completed: boolean("completed").default(false).notNull(),
priority: text("priority", {
enum: ["low", "medium", "high"],
}).default("medium").notNull(),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
})
ตรงนี้เราใช้ pgTable ซึ่งเป็น PostgreSQL-specific helper ของ Drizzle สิ่งที่น่าสนใจคือ:
uuid().defaultRandom()— สร้าง UUID v4 อัตโนมัติเมื่อ insert ข้อมูลใหม่text("priority", { enum: [...] })— กำหนด enum values ใน TypeScript ได้เลย Drizzle จะสร้าง type union ให้อัตโนมัติtimestamp().defaultNow()— ใส่วันเวลาปัจจุบันให้อัตโนมัติ ไม่ต้องจัดการเอง
สร้าง Zod Validation Schema จาก Drizzle Schema
นี่คือจุดที่ Drizzle โดดเด่นมากจริงๆ — ใช้ drizzle-zod สร้าง Zod schema จาก database schema ได้ทันที โดยไม่ต้องเขียนซ้ำให้เสียเวลา:
// db/schema.ts (ต่อจากด้านบน)
import { createInsertSchema, createSelectSchema } from "drizzle-zod"
import { z } from "zod"
// Schema สำหรับ validate ข้อมูลที่จะ insert
export const insertTaskSchema = createInsertSchema(tasks, {
title: (schema) => schema.min(1, "กรุณาใส่ชื่อ Task").max(200, "ชื่อ Task ยาวเกินไป"),
description: (schema) => schema.max(1000, "คำอธิบายยาวเกินไป").optional(),
}).omit({
id: true,
createdAt: true,
updatedAt: true,
})
// Schema สำหรับ validate ข้อมูลที่ query ออกมา
export const selectTaskSchema = createSelectSchema(tasks)
// สร้าง TypeScript types จาก schema
export type InsertTask = z.infer<typeof insertTaskSchema>
export type Task = z.infer<typeof selectTaskSchema>
ข้อดีที่เห็นชัดมาก: เมื่อแก้ไข database schema เช่น เพิ่มคอลัมน์ใหม่ Zod schema จะอัพเดทตามอัตโนมัติ ไม่ต้องมานั่ง sync ระหว่าง schema กับ validation เอง ซึ่งเป็นปัญหาคลาสสิคที่เจอบ่อยมากในโปรเจกต์ขนาดใหญ่ (เคยเจอ bug จากเรื่องนี้ไม่รู้กี่ครั้ง)
4. ตั้งค่า Database Connection
สร้างไฟล์เชื่อมต่อ database ที่ใช้ Neon serverless driver ครับ:
// db/index.ts
import { neon } from "@neondatabase/serverless"
import { drizzle } from "drizzle-orm/neon-http"
import * as schema from "./schema"
const sql = neon(process.env.DATABASE_URL!)
export const db = drizzle(sql, { schema })
แค่ 4 บรรทัดก็ได้ database connection ที่พร้อมใช้งานแล้ว สั้นมาก
Neon serverless driver (@neondatabase/serverless) ส่ง query ผ่าน HTTP ซึ่งเหมาะมากกับ serverless environment เพราะไม่ต้องเปิด persistent TCP connection — ทุก request จะเป็น stateless HTTP call ไปยัง Neon โดยตรง
ทำไมใช้ neon-http แทน postgres-js?
neon-http— ส่ง query ผ่าน HTTP ไม่ต้อง connection pool เหมาะกับ Vercel Functions และ serverlesspostgres-js— ใช้ TCP connection ต้องจัดการ connection pool เอง เหมาะกับ long-running server
ถ้า deploy บน Vercel ใช้ neon-http เถอะ เหมาะที่สุดแล้ว
5. สร้างและ Run Migrations
ตั้งค่า Drizzle Kit สำหรับจัดการ migrations:
// drizzle.config.ts
import { defineConfig } from "drizzle-kit"
export default defineConfig({
schema: "./db/schema.ts",
out: "./db/migrations",
dialect: "postgresql",
dbCredentials: {
url: process.env.DATABASE_URL!,
},
strict: true,
verbose: true,
})
สำคัญมากนะครับ: ต้องใส่ strict: true เสมอ เพราะถ้าไม่ใส่ Drizzle Kit อาจตีความการเปลี่ยนชื่อคอลัมน์เป็น "ลบแล้วสร้างใหม่" ซึ่งจะทำให้ข้อมูลหายได้ เจอแบบนี้ในตอนแรกเลยกว่าจะรู้
เพิ่ม scripts ใน package.json:
{
"scripts": {
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "drizzle-kit studio"
}
}
จากนั้น generate migration files และ push ไปยัง database:
# Generate migration files จาก schema
npm run db:generate
# Push schema ไปยัง Neon database
npm run db:push
คำสั่ง db:push จะ sync schema ไปยัง database โดยตรง เหมาะสำหรับช่วง development ส่วน db:generate + db:migrate เหมาะสำหรับ production ที่ต้องการ migration history ตรงนี้อย่าสับสนนะครับ
ทดสอบว่า database พร้อมใช้งานด้วย Drizzle Studio:
npm run db:studio
Drizzle Studio จะเปิด UI ในเบราว์เซอร์ให้ดูข้อมูลใน database ได้เลย ลองตรวจสอบว่า table tasks ถูกสร้างเรียบร้อยก่อนไปขั้นตอนถัดไป
6. สร้าง Server Actions สำหรับ CRUD Operations
ตรงนี้คือส่วนสนุกแล้วครับ — เราจะรวม Drizzle ORM เข้ากับ Next.js 16 Server Actions ทำให้จัดการ database ได้โดยตรงจาก React components เลย:
// lib/actions.ts
"use server"
import { db } from "@/db"
import { tasks, insertTaskSchema } from "@/db/schema"
import { eq } from "drizzle-orm"
import { revalidatePath } from "next/cache"
// ============ CREATE ============
export async function createTask(formData: FormData) {
const rawData = {
title: formData.get("title") as string,
description: formData.get("description") as string || undefined,
priority: formData.get("priority") as "low" | "medium" | "high",
}
// Validate ด้วย Zod schema ที่สร้างจาก Drizzle
const result = insertTaskSchema.safeParse(rawData)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
}
}
await db.insert(tasks).values(result.data)
revalidatePath("/tasks")
return { success: true, errors: null }
}
// ============ READ ============
export async function getTasks() {
return await db
.select()
.from(tasks)
.orderBy(tasks.createdAt)
}
export async function getTaskById(id: string) {
const result = await db
.select()
.from(tasks)
.where(eq(tasks.id, id))
.limit(1)
return result[0] ?? null
}
// ============ UPDATE ============
export async function updateTask(id: string, formData: FormData) {
const rawData = {
title: formData.get("title") as string,
description: formData.get("description") as string || undefined,
priority: formData.get("priority") as "low" | "medium" | "high",
completed: formData.get("completed") === "true",
}
const result = insertTaskSchema.safeParse(rawData)
if (!result.success) {
return {
success: false,
errors: result.error.flatten().fieldErrors,
}
}
await db
.update(tasks)
.set({ ...result.data, updatedAt: new Date() })
.where(eq(tasks.id, id))
revalidatePath("/tasks")
return { success: true, errors: null }
}
// ============ DELETE ============
export async function deleteTask(id: string) {
await db.delete(tasks).where(eq(tasks.id, id))
revalidatePath("/tasks")
return { success: true }
}
// ============ TOGGLE COMPLETE ============
export async function toggleTaskComplete(id: string) {
const task = await getTaskById(id)
if (!task) return { success: false }
await db
.update(tasks)
.set({
completed: !task.completed,
updatedAt: new Date(),
})
.where(eq(tasks.id, id))
revalidatePath("/tasks")
return { success: true }
}
จะเห็นว่าโค้ดสะอาดมากครับ Drizzle query syntax ใกล้เคียง SQL จริงๆ เช่น db.select().from(tasks).where(eq(tasks.id, id)) — อ่านแล้วเข้าใจทันทีว่ากำลังทำอะไร ไม่ต้องไปเปิดเอกสารดู API
สิ่งที่ควรสังเกต:
revalidatePath("/tasks")— หลังจาก mutate ข้อมูลแล้ว ต้อง revalidate path เพื่อให้ Next.js รู้ว่าข้อมูลเปลี่ยน ต้อง re-render ใหม่insertTaskSchema.safeParse()— ใช้ Zod schema ที่สร้างจาก Drizzle มา validate ข้อมูลก่อน insert/update เสมอ อย่าข้ามขั้นตอนนี้นะครับ- Server Actions ทั้งหมดถูก mark ด้วย
"use server"ที่บรรทัดแรก ทำให้ Next.js สร้าง POST endpoint ให้อัตโนมัติ
7. สร้าง UI Components
Task Form Component
มาสร้างฟอร์มสำหรับเพิ่ม/แก้ไข task กัน โดยใช้ useActionState จาก React 19:
// components/TaskForm.tsx
"use client"
import { useActionState } from "react"
import { createTask } from "@/lib/actions"
const initialState = { success: false, errors: null as Record<string, string[]> | null }
export function TaskForm() {
const [state, formAction, isPending] = useActionState(
async (_prevState: typeof initialState, formData: FormData) => {
return await createTask(formData)
},
initialState
)
return (
<form action={formAction}>
<div>
<label htmlFor="title">ชื่อ Task</label>
<input
id="title"
name="title"
type="text"
required
placeholder="เช่น ทำการบ้าน, ส่ง report..."
/>
{state.errors?.title && (
<p style={{ color: "red" }}>{state.errors.title[0]}</p>
)}
</div>
<div>
<label htmlFor="description">คำอธิบาย (ไม่บังคับ)</label>
<textarea
id="description"
name="description"
placeholder="รายละเอียดเพิ่มเติม..."
/>
</div>
<div>
<label htmlFor="priority">ความสำคัญ</label>
<select id="priority" name="priority" defaultValue="medium">
<option value="low">ต่ำ</option>
<option value="medium">ปานกลาง</option>
<option value="high">สูง</option>
</select>
</div>
<button type="submit" disabled={isPending}>
{isPending ? "กำลังบันทึก..." : "เพิ่ม Task"}
</button>
{state.success && (
<p style={{ color: "green" }}>เพิ่ม Task สำเร็จ!</p>
)}
</form>
)
}
Task List Component (Server Component)
นี่คือจุดที่ Server Components เปล่งพลังเต็มที่ครับ — query ข้อมูลจาก database ได้โดยตรงใน component เลย ไม่ต้องผ่าน API route ให้ยุ่งยาก:
// components/TaskList.tsx
import { db } from "@/db"
import { tasks } from "@/db/schema"
import { desc } from "drizzle-orm"
import { TaskItem } from "./TaskItem"
export async function TaskList() {
const allTasks = await db
.select()
.from(tasks)
.orderBy(desc(tasks.createdAt))
if (allTasks.length === 0) {
return <p>ยังไม่มี Task — เริ่มเพิ่ม Task แรกกันเลย!</p>
}
return (
<ul>
{allTasks.map((task) => (
<TaskItem key={task.id} task={task} />
))}
</ul>
)
}
แค่นี้เลย ไม่ต้องสร้าง API route แยก ไม่ต้อง fetch ฝั่ง client สะอาดมาก
Task Item Component (Client Component)
สร้าง component สำหรับแสดงแต่ละ task พร้อมปุ่ม toggle complete และ delete:
// components/TaskItem.tsx
"use client"
import { toggleTaskComplete, deleteTask } from "@/lib/actions"
import type { Task } from "@/db/schema"
export function TaskItem({ task }: { task: Task }) {
return (
<li style={{
opacity: task.completed ? 0.6 : 1,
textDecoration: task.completed ? "line-through" : "none"
}}>
<div>
<strong>{task.title}</strong>
{task.description && <p>{task.description}</p>}
<span>ความสำคัญ: {task.priority}</span>
</div>
<div>
<form action={async () => {
"use server"
await toggleTaskComplete(task.id)
}}>
<button type="submit">
{task.completed ? "ยกเลิกเสร็จ" : "เสร็จแล้ว"}
</button>
</form>
<form action={async () => {
"use server"
await deleteTask(task.id)
}}>
<button type="submit">ลบ</button>
</form>
</div>
</li>
)
}
หน้า Tasks
รวมทุกอย่างเข้าด้วยกันในหน้าหลัก:
// app/tasks/page.tsx
import { Suspense } from "react"
import { TaskForm } from "@/components/TaskForm"
import { TaskList } from "@/components/TaskList"
export default function TasksPage() {
return (
<main>
<h1>Task Manager</h1>
<section>
<h2>เพิ่ม Task ใหม่</h2>
<TaskForm />
</section>
<section>
<h2>รายการ Tasks</h2>
<Suspense fallback={<p>กำลังโหลด...</p>}>
<TaskList />
</Suspense>
</section>
</main>
)
}
สังเกตว่า <TaskList /> ถูกห่อด้วย <Suspense> ทำให้ผู้ใช้เห็น "กำลังโหลด..." ขณะที่ Server Component กำลัง query ข้อมูลจาก database อยู่ ส่วน <TaskForm /> เป็น Client Component ที่ render ได้ทันทีโดยไม่ต้องรอ data — UX ดีขึ้นเยอะเลยครับ
8. Relational Queries — ขยายให้ซับซ้อนขึ้น
ในโปรเจกต์จริงๆ มักจะมีหลาย table ที่เชื่อมโยงกัน ไม่ได้มีแค่ table เดียวเหมือนตัวอย่าง สมมติว่าเราเพิ่ม table สำหรับ task categories เข้ามา:
// db/schema.ts (เพิ่มเติม)
import { pgTable, text, timestamp, boolean, uuid } from "drizzle-orm/pg-core"
import { relations } from "drizzle-orm"
export const categories = pgTable("categories", {
id: uuid("id").defaultRandom().primaryKey(),
name: text("name").notNull(),
color: text("color").default("#3b82f6").notNull(),
})
export const tasks = pgTable("tasks", {
id: uuid("id").defaultRandom().primaryKey(),
title: text("title").notNull(),
description: text("description"),
completed: boolean("completed").default(false).notNull(),
priority: text("priority", {
enum: ["low", "medium", "high"],
}).default("medium").notNull(),
categoryId: uuid("category_id").references(() => categories.id),
createdAt: timestamp("created_at").defaultNow().notNull(),
updatedAt: timestamp("updated_at").defaultNow().notNull(),
})
// กำหนดความสัมพันธ์
export const categoriesRelations = relations(categories, ({ many }) => ({
tasks: many(tasks),
}))
export const tasksRelations = relations(tasks, ({ one }) => ({
category: one(categories, {
fields: [tasks.categoryId],
references: [categories.id],
}),
}))
จากนั้นใช้ Relational Query Builder เพื่อ query ข้อมูลพร้อม relation ได้แบบนี้:
// ดึง tasks พร้อมข้อมูล category
const tasksWithCategories = await db.query.tasks.findMany({
with: {
category: true,
},
orderBy: (tasks, { desc }) => [desc(tasks.createdAt)],
})
// ดึง categories พร้อม tasks ทั้งหมด
const categoriesWithTasks = await db.query.categories.findMany({
with: {
tasks: {
where: (tasks, { eq }) => eq(tasks.completed, false),
},
},
})
พูดตรงๆ Drizzle Relational Query Builder ทำให้ query ข้อมูลแบบ nested ง่ายขึ้นเยอะมาก ไม่ต้องมานั่งเขียน JOIN เอง แถม type safety ก็ยังคงอยู่ครบถ้วนด้วย ถ้าเคยเขียน raw SQL join แล้วต้องมา type ผลลัพธ์เอง จะรู้สึกว่าสบายขึ้นมาก
9. เพิ่ม Caching ด้วย use cache ของ Next.js 16
อันนี้เป็นฟีเจอร์ที่ชอบมาก ถ้าคุณเปิดใช้ cacheComponents ใน Next.js 16 แล้ว สามารถ cache ผลลัพธ์ของ database queries ได้ง่ายๆ แบบนี้:
// components/TaskStats.tsx
import { cacheLife, cacheTag } from "next/cache"
import { db } from "@/db"
import { tasks } from "@/db/schema"
import { eq, count } from "drizzle-orm"
export async function TaskStats() {
"use cache"
cacheLife("minutes")
cacheTag("task-stats")
const [totalResult] = await db
.select({ count: count() })
.from(tasks)
const [completedResult] = await db
.select({ count: count() })
.from(tasks)
.where(eq(tasks.completed, true))
return (
<div>
<p>Tasks ทั้งหมด: {totalResult.count}</p>
<p>เสร็จแล้ว: {completedResult.count}</p>
<p>ยังไม่เสร็จ: {totalResult.count - completedResult.count}</p>
</div>
)
}
จากนั้นเมื่อ mutate ข้อมูล ก็แค่ revalidate cache tag:
// lib/actions.ts (เพิ่ม)
import { revalidateTag } from "next/cache"
export async function createTask(formData: FormData) {
// ... โค้ดเดิม ...
revalidatePath("/tasks")
revalidateTag("task-stats") // revalidate stats cache ด้วย
return { success: true, errors: null }
}
การผสมผสาน Drizzle ORM กับ use cache ของ Next.js 16 ทำให้ได้ทั้ง performance ที่ดีจาก caching และข้อมูลที่ fresh จาก on-demand revalidation ลงตัวมากครับ
10. Deploy บน Vercel
ขั้นตอนสุดท้ายแล้ว การ deploy แอป Next.js + Drizzle + Neon บน Vercel ตรงไปตรงมามาก:
- Push code ขึ้น GitHub
- Import project ใน Vercel — ไปที่ vercel.com แล้วเลือก Import Git Repository
- ตั้งค่า Environment Variables — เพิ่ม
DATABASE_URLที่ได้จาก Neon - Deploy! — กดปุ่มเดียวจบ
หรือถ้าชอบใช้ command line อย่างผม ใช้ Vercel CLI ก็ได้:
npm i -g vercel
vercel --prod
ข้อดีของ stack นี้ตอน deploy:
- Bundle size เล็ก — Drizzle ORM มี bundle size เล็กกว่า Prisma มาก ทำให้ cold start เร็วกว่าบน Vercel Functions อย่างเห็นได้ชัด
- ไม่ต้องจัดการ connection pool — Neon serverless driver ส่ง query ผ่าน HTTP ไม่ต้องกังวลเรื่อง pool connection
- Scale-to-zero — Neon จะ scale database ลงเป็น 0 เมื่อไม่มี traffic ประหยัดค่าใช้จ่ายในช่วง dev ได้เยอะ
Drizzle ORM กับ Prisma: เปรียบเทียบสั้นๆ
หลายคนคงสงสัยว่าทำไมเลือก Drizzle แทน Prisma? มาดูตารางเปรียบเทียบกันครับ:
| เกณฑ์ | Drizzle ORM | Prisma |
|---|---|---|
| Bundle size | เล็กมาก (~50KB) | ใหญ่กว่า (หลาย MB) |
| Cold start | เร็ว | ช้ากว่า |
| SQL Control | สูง — เหมือนเขียน SQL | ต่ำ — ใช้ Prisma Client API |
| Edge Runtime | รองรับ native | ต้องใช้ Prisma Accelerate |
| Learning Curve | ต้องรู้ SQL | ง่ายกว่าสำหรับผู้เริ่มต้น |
| Serverless DB | รองรับ Neon, Turso, D1 native | ต้องใช้ adapter เพิ่ม |
| Type Safety | ครบถ้วน | ครบถ้วน |
สรุป: ถ้าคุณ deploy บน Vercel/serverless และต้องการ performance สูงสุด Drizzle ORM เป็นตัวเลือกที่ดีกว่า แต่ถ้าต้องการความง่ายและ ecosystem ที่ mature กว่า Prisma ก็ยังเป็นตัวเลือกที่ดี โดยเฉพาะ Prisma 7 ที่เปลี่ยนมาเป็น pure TypeScript ทำให้ช่องว่างระหว่างสองตัวนี้แคบลงเยอะ ว่าไปแล้วจะเลือกตัวไหนก็ไม่ผิดครับ ขึ้นอยู่กับ preference ของทีม
คำถามที่พบบ่อย (FAQ)
Drizzle ORM คืออะไร และต่างจาก Prisma อย่างไร?
Drizzle ORM เป็น TypeScript ORM ที่ใช้ SQL-first approach คือ syntax ใกล้เคียง SQL จริงๆ ทำให้ developer ที่คุ้น SQL ใช้ง่ายมาก ต่างจาก Prisma ที่ใช้ schema-first approach กับ Prisma Client API แบบ custom ข้อได้เปรียบหลักของ Drizzle คือ bundle size ที่เล็กกว่ามาก เหมาะกับ serverless และ edge runtime ส่วน Prisma เด่นเรื่อง ecosystem ที่กว้างกว่าและเรียนรู้ง่ายกว่าสำหรับคนที่ไม่ถนัด SQL
ทำไมต้องใช้ Neon แทน Supabase หรือ PlanetScale?
Neon เหมาะกับ Next.js + Vercel เป็นพิเศษ เพราะมี serverless driver ที่ส่ง query ผ่าน HTTP ได้โดยตรง ไม่ต้อง connection pool มี scale-to-zero ที่ช่วยประหยัดค่าใช้จ่ายตอน dev และมี database branching สำหรับทดสอบ schema changes ส่วน Supabase เหมาะกว่าถ้าต้องการ BaaS (Backend as a Service) แบบครบวงจร เช่น auth, storage, realtime ในตัว
drizzle-zod คืออะไร และช่วยอะไรได้บ้าง?
drizzle-zod เป็น plugin ที่สร้าง Zod validation schema จาก Drizzle database schema โดยอัตโนมัติ ช่วยให้ไม่ต้องเขียน validation schema ซ้ำ เมื่อ database schema เปลี่ยน validation ก็อัพเดทตาม มีสองฟังก์ชันหลักคือ createInsertSchema (สำหรับ validate ข้อมูลขาเข้า) และ createSelectSchema (สำหรับ validate ข้อมูลที่ query ออกมา) ใน Drizzle v1 ตัวนี้จะถูกรวมเข้า core package เป็น drizzle-orm/zod เลย
ใช้ Drizzle ORM กับ Edge Runtime ของ Next.js ได้ไหม?
ได้ครับ Drizzle ORM รองรับ Edge Runtime ได้ native เลย เพราะ bundle size เล็กและไม่มี native binary เหมือน Prisma แค่ใช้ Neon serverless driver หรือ driver ที่รองรับ Edge อย่าง Turso, Cloudflare D1 ก็ใช้ได้ทันที ต่างจาก Prisma ที่ต้องใช้ Prisma Accelerate เพิ่มเติม
ควรใช้ db:push หรือ db:migrate ใน production?
สำหรับ production ควรใช้ db:generate + db:migrate เสมอครับ เพราะจะสร้าง migration files ที่ track ประวัติการเปลี่ยนแปลง schema ทำให้ rollback ได้ถ้ามีปัญหา ส่วน db:push เหมาะสำหรับ development เท่านั้น เพราะจะ sync schema ไปยัง database โดยตรงโดยไม่สร้าง migration history อย่าเอาไปใช้กับ production เด็ดขาดนะ