Phân tích toàn diện Next.js 15 App Router + Server Actions + PPR, TypeScript strict mode cho team scaling, PostgreSQL JSONB/pgvector/ACID, so sánh Drizzle vs Prisma, shadcn/ui + Tailwind v4, Supabase Auth vs Auth.js, production boilerplate, và AI-Ready RAG architecture.
"Chọn sai stack giống như xây nhà trên nền cát — bạn không thấy vấn đề cho đến khi đã đầu tư quá nhiều để quay đầu."
Năm 2025-2026, cuộc chiến tech stack đã ngã ngũ. State of JS 2024, Stack Overflow Developer Survey, và hàng nghìn startup thực chiến đều đồng thuận: Next.js + TypeScript + PostgreSQL là bộ ba thống trị.
Nhưng biết "cái gì" không đủ. Bài viết này giải thích tại sao — từ góc độ kỹ thuật thực tế của một team xây MVP hàng tuần, không phải từ góc độ marketing của các framework.
1. Next.js 15: Vượt Qua Ranh Giới Frontend/Backend
App Router — Cuộc Cách Mạng Thực Sự
Next.js 15 với App Router không chỉ là routing mới. Nó là mô hình tư duy hoàn toàn khác: React Server Components (RSC) như mặc định.
Trong Pages Router cũ, mọi component đều chạy trên client. Trong App Router:
app/
├── layout.tsx ← Server Component (HTML shell)
├── page.tsx ← Server Component by default
├── dashboard/
│ ├── page.tsx ← Server Component
│ └── ClientChart.tsx ← 'use client' chỉ khi cần interactivity
└── api/
└── webhook/
└── route.ts ← API Route handler
Lợi ích thực tế của RSC:
// app/dashboard/page.tsx — Server Component// Không cần useState, useEffect, loading states, error handling ở client// Database query chạy server-side — data không bao giờ expose ra clientimport { db } from'@/lib/db'exportdefaultasyncfunctionDashboardPage() {
// Direct database query — không cần API routeconst stats = await db.query.metrics.findMany({
where: (m, { gte }) =>gte(m.createdAt, newDate(Date.() - )),
: (m.),
: ,
})
(
)
}
TypeScript types tự động flow từ server sang client
Progressive enhancement: hoạt động ngay cả khi JS disabled
CSRF protection built-in
PPR — Partial Prerendering (Next.js 15 Stable)
PPR là hybrid rendering model: render phần static ngay lập tức (0ms), stream phần dynamic vào sau.
// app/product/[id]/page.tsximport { Suspense } from'react'// Phần static — render compile timeexportdefaultasyncfunctionProductPage({ params }) {
return (
<div>
{/* Static: render ngay, cached forever */}
<ProductHeroproductId={params.id} />
{/* Dynamic: stream sau khi có data */}
<Suspensefallback={<ReviewsSkeleton />}>
<ProductReviewsproductId={params.id} /></Suspense><Suspensefallback={<PricingSkeleton />}>
<DynamicPricingproductId={params.id} /></Suspense></div>
)
}
Kết quả: User nhìn thấy product info ngay lập tức (từ static cache), trong khi reviews và pricing load động. Perceived performance cực kỳ tốt mà không cần compromise trên dynamic data.
Turbopack — Dev Experience 5x Nhanh
Next.js 15 stable với Turbopack (Rust-based bundler):
strictNullChecks: Không thể dùng undefined mà không check
strictFunctionTypes: Function parameter types phải khớp chặt
strictBindCallApply: Type-check cho .bind(), .call(), .apply()
noImplicitAny: Không được để TypeScript tự infer any
strictPropertyInitialization: Class properties phải được init
noImplicitThis: this phải có type rõ ràng
Utility Types Thực Tế trong MVP
// Từ database schematypeUser = {
id: stringemail: stringpasswordHash: stringcreatedAt: DateupdatedAt: Date
}
// Không bao giờ expose passwordHash ra ngoàitypePublicUser = Omit<User, 'passwordHash'>
// Khi tạo user mới — id, createdAt, updatedAt tự generatetypeCreateUserInput = Omit<User, 'id' | 'createdAt' | 'updatedAt'>
// Khi update — mọi field đều optionaltypeUpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>
// API Response wrapper type-safetypeApiResponse<T> =
| { success: true; data: T }
| { success: false; error: string; code: number }
// React component với strict propstypeButtonProps = {
variant: 'primary' | 'secondary' | 'danger'size: 'sm' | 'md' | 'lg'loading?: booleanonClick?: () =>voidchildren: React.ReactNode
} & Omit<React.ButtonHTMLAttributes<HTMLButtonElement>, 'onClick'>
Zod + TypeScript: Schema Validation + Type Inference
import { z } from'zod'// Định nghĩa 1 lần — dùng cho cả validation lẫn TypeScript typeconstCreateProjectSchema = z.object({
name: z.string().min(1, 'Tên không được để trống').max(100),
description: z.string().max(500).optional(),
budget: z.number().min(0).max(1_000_000_000),
deadline: z.string().datetime().optional(),
tags: z.array(z.string()).max(10).default([]),
})
// TypeScript type tự động từ schematypeCreateProjectInput = z.infer<typeofCreateProjectSchema>
// Dùng trong Server ActionexportasyncfunctioncreateProject(input: CreateProjectInput) {
// input đã được validate, TypeScript biết exact shape
}
3. PostgreSQL: Vô Đối Sau 35 Năm — Và Ngày Càng Mạnh Hơn
Tại Sao Không Dùng MongoDB, DynamoDB, hay PlanetScale?
Câu hỏi nghe quen thuộc. Câu trả lời sau khi migrating nhiều projects:
Tiêu chí
PostgreSQL
MongoDB
DynamoDB
ACID transactions
Full
Partial (single doc)
Limited
Relational queries
Native JOIN
Aggregation pipeline (verbose)
Scan heavy
JSON support
JSONB (indexed)
Native
AttributeValue
Full-text search
Built-in
Atlas Search ($$)
OpenSearch needed
Vector similarity
pgvector
Atlas Vector Search ($$)
External needed
Schema flexibility
JSONB columns
Schemaless
Schemaless
Query complexity
SQL (universal)
MQL (proprietary)
PartiQL (limited)
Serverless
Neon, Supabase
Atlas Serverless
Native
Cost at scale
Linear
Expensive at scale
Expensive at scale
PostgreSQL wins on almost every dimension that matters for a startup that will eventually need to do complex queries.
JSONB — Linh Hoạt Như NoSQL, Mạnh Như Relational
-- Bảng users với JSONB metadataCREATE TABLE users (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
email TEXT UNIQUENOT NULL,
metadata JSONB DEFAULT'{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- Tạo GIN index cho JSONB — query nhanh dù không biết structure trướcCREATE INDEX idx_users_metadata ON users USING GIN (metadata);
-- Insert với nested JSONINSERT INTO users (email, metadata) VALUES (
'[email protected]',
'{"plan": "pro", "features": ["ai", "analytics"], "company": {"name": "ACME", "size": 50}}'
);
-- Query vào nested JSONSELECT*FROM users
WHERE metadata->>'plan'='pro'AND metadata->'company'->>'size'>'20';
-- Update nested field mà không cần fetch rồi update toàn bộ documentUPDATE users
SET metadata = jsonb_set(metadata, '{company,size}', '100')
WHERE id ='some-uuid';
-- Array operationsSELECT*FROM users
WHERE metadata->'features' ? 'ai'; -- có feature 'ai' trong array
JSONB cho phép bạn ship nhanh (không cần migrate schema mỗi lần thay đổi requirements) mà vẫn giữ được relational joins và ACID guarantees.
pgvector — PostgreSQL Trở Thành Vector Database
-- Install extensionCREATE EXTENSION IF NOTEXISTS vector;
-- Bảng documents với vector embeddingsCREATE TABLE documents (
id UUID PRIMARY KEYDEFAULT gen_random_uuid(),
content TEXT NOT NULL,
embedding VECTOR(1536), -- OpenAI ada-002 dimension
metadata JSONB DEFAULT'{}',
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- HNSW index cho approximate nearest neighbor search-- Nhanh hơn ivfflat, build time chậm hơn nhưng query nhanh hơnCREATE INDEX ON documents USING hnsw (embedding vector_cosine_ops)
WITH (m =16, ef_construction =64);
-- Semantic similarity search (RAG)SELECT
content,
metadata,
1- (embedding <=> $1::vector) AS similarity
FROM documents
WHERE1- (embedding <=> $1::vector) >0.7-- thresholdORDERBY embedding <=> $1::vector
LIMIT 5;
Điều này có nghĩa là: bạn không cần Pinecone, Weaviate, hay Qdrant. PostgreSQL + pgvector đủ cho hầu hết RAG use cases của startup.
Window Functions và CTEs — Power Queries
-- Running total doanh thu theo ngàySELECT
DATE_TRUNC('day', created_at) ASday,
SUM(amount) AS daily_revenue,
SUM(SUM(amount)) OVER (ORDERBY DATE_TRUNC('day', created_at)) AS cumulative_revenue
FROM payments
WHERE status ='completed'GROUPBY1ORDERBY1;
-- Top user mỗi tháng (ranking)WITH monthly_activity AS (
SELECT
user_id,
DATE_TRUNC('month', created_at) ASmonth,
COUNT(*) AS action_count
FROM user_events
GROUPBY1, 2
),
ranked AS (
SELECT*,
RANK() OVER (PARTITIONBYmonthORDERBY action_count DESC) AS rank
FROM monthly_activity
)
SELECT*FROM ranked WHERE rank <=10;
Prisma là ORM phổ biến nhất cho Node.js. Nhưng có những vấn đề thực tế:
Schema định nghĩa riêng (không phải TypeScript):
// schema.prisma — ngôn ngữ riêng, không phải TS
model User {
id String @id @default(cuid())
email String @unique
posts Post[]
createdAt DateTime @default(now())
}
Vấn đề với Prisma:
Prisma Client generate code to /node_modules — slow cold starts (quan trọng với serverless/edge)
Database connection pooling configured (PgBouncer hoặc Neon pooler)
Images served từ CDN, không phải /public
console.log removed (dùng proper logger như Pino)
Error monitoring setup (Sentry free tier)
Rate limiting trên API routes (upstash/ratelimit)
Webhook signature validation (Stripe, etc.)
CSP headers configured
Database indexes trên foreign keys và frequently queried columns
Kết Luận: Tại Sao Stack Này Vẫn Thắng Trong 2026
Không có stack hoàn hảo. Nhưng có stack đủ tốt cho phần lớn use cases, với ecosystem lớn nhất, talent pool dễ hire nhất, và documentation tốt nhất.
Next.js + TypeScript + PostgreSQL đáp ứng cả ba tiêu chí đó — và với sự bổ sung của pgvector, Vercel AI SDK, và Drizzle, nó còn trở thành AI-native stack mà không cần thêm bất kỳ external service nào.
Trong bối cảnh startup cần move fast:
Next.js 15 loại bỏ ranh giới frontend/backend
TypeScript biến codebase thành documentation tự động
PostgreSQL + pgvector là database duy nhất bạn cần cho cả OLTP lẫn AI workloads
Drizzle giữ type safety từ database ra đến UI
shadcn/ui ship production UI trong giờ, không phải ngày
Đây không phải là stack của năm 2020 hay 2022. Đây là stack của 2026 và những năm tiếp theo — được thiết kế cho AI-first, edge-first, developer-experience-first world.