ในปี 2026 Next.js กลายเป็น Framework อันดับหนึ่งสำหรับการพัฒนา Web Application ด้วย React ไม่ว่าจะเป็นเว็บไซต์ขนาดเล็กหรือระบบ Enterprise ขนาดใหญ่ Next.js ตอบโจทย์ได้หมด ตั้งแต่ Static Site ไปจนถึง Full-Stack Application ที่มี API, Authentication และ Database ครบครัน
บทความนี้จะพาคุณเรียนรู้ Next.js 14 ตั้งแต่พื้นฐานจนถึงการสร้าง Full-Stack Application จริง ครอบคลุมทุกฟีเจอร์สำคัญรวมถึง App Router, Server Components, Data Fetching, API Routes และ Deployment อย่างละเอียดในภาษาไทย
Next.js คืออะไร?
Next.js คือ React Framework ที่พัฒนาโดย Vercel ออกแบบมาเพื่อให้การสร้าง Web Application ด้วย React ง่ายขึ้นและมีประสิทธิภาพมากขึ้น โดยเพิ่มฟีเจอร์สำคัญที่ React เพียวไม่มีให้ เช่น Server-Side Rendering (SSR), Static Site Generation (SSG), File-based Routing, API Routes และ Built-in Optimization ต่างๆ
Next.js แก้ปัญหาหลายอย่างที่ Developer เจอเมื่อใช้ React แบบ Client-Side เพียวอย่างเดียว:
- SEO: React SPA ไม่เป็นมิตรกับ Search Engine เพราะ HTML ว่างเปล่า Next.js Render HTML บน Server ก่อนส่งให้ Browser
- Performance: Server Components ทำให้ JavaScript Bundle เล็กลงมาก ลด Time to Interactive
- Routing: ไม่ต้องติดตั้ง React Router แยก ใช้ File-based Routing ที่เข้าใจง่าย
- Full-Stack: เขียน API ใน Project เดียวกันได้เลย ไม่ต้องแยก Backend
- Image Optimization: ย่อขนาดรูป, Lazy Loading, WebP/AVIF conversion อัตโนมัติ
- Developer Experience: Fast Refresh, TypeScript Support, Built-in CSS/Sass Support
Next.js vs Create React App (CRA)
| คุณสมบัติ | Next.js | Create React App |
|---|---|---|
| Rendering | SSR, SSG, ISR, CSR | CSR เท่านั้น |
| Routing | File-based (built-in) | ต้องติดตั้ง React Router |
| SEO | ดีเยี่ยม (Server Rendering) | แย่ (Client-side only) |
| API Routes | มี Built-in | ไม่มี ต้องแยก Backend |
| Image Optimization | Built-in next/image | ไม่มี |
| Bundle Size | เล็กกว่า (Code Splitting auto) | ใหญ่กว่า |
| Deployment | Vercel, Self-hosted, Docker | Static hosting |
| สถานะปี 2026 | Active พัฒนาต่อเนื่อง | Deprecated ไม่แนะนำ |
เริ่มต้นกับ Next.js 14
ติดตั้ง Next.js
# สร้างโปรเจกต์ใหม่ด้วย create-next-app
npx create-next-app@latest my-app
# ตัวเลือกที่แนะนำ:
# TypeScript: Yes
# ESLint: Yes
# Tailwind CSS: Yes
# src/ directory: Yes
# App Router: Yes
# Import alias: @/*
cd my-app
npm run dev # เปิด Development Server ที่ http://localhost:3000
โครงสร้างโปรเจกต์
my-app/
├── src/
│ ├── app/ # App Router (หัวใจของ Next.js 14)
│ │ ├── layout.tsx # Root Layout (ครอบทุกหน้า)
│ │ ├── page.tsx # Home page (/)
│ │ ├── globals.css # Global styles
│ │ ├── about/
│ │ │ └── page.tsx # /about
│ │ ├── blog/
│ │ │ ├── page.tsx # /blog
│ │ │ └── [slug]/
│ │ │ └── page.tsx # /blog/:slug (Dynamic route)
│ │ └── api/
│ │ └── hello/
│ │ └── route.ts # API endpoint: /api/hello
│ ├── components/ # Shared components
│ └── lib/ # Utility functions
├── public/ # Static assets
├── next.config.js # Next.js configuration
├── tailwind.config.ts # Tailwind CSS config
├── tsconfig.json # TypeScript config
└── package.json
App Router vs Pages Router
Next.js มี 2 ระบบ Routing คือ App Router (ใหม่ แนะนำ) และ Pages Router (เก่า ยังใช้ได้) ในบทความนี้จะเน้น App Router ซึ่งเป็น Default ตั้งแต่ Next.js 13
| คุณสมบัติ | App Router (app/) | Pages Router (pages/) |
|---|---|---|
| React Components | Server Components (default) | Client Components (default) |
| Data Fetching | async/await ใน Component | getServerSideProps, getStaticProps |
| Layouts | Nested Layouts (layout.tsx) | _app.tsx, _document.tsx |
| Loading UI | loading.tsx (Suspense) | ต้องทำเอง |
| Error Handling | error.tsx (Error Boundary) | ต้องทำเอง |
| API Routes | route.ts (Route Handlers) | pages/api/*.ts |
| Streaming | รองรับ | ไม่รองรับ |
File-based Routing ระบบเส้นทางอัตโนมัติ
Next.js App Router ใช้ระบบ File-based Routing ที่สร้าง URL จากโครงสร้างโฟลเดอร์อัตโนมัติ แค่สร้างไฟล์ page.tsx ในโฟลเดอร์ ก็จะได้ Route ทันที
Static Routes
// src/app/page.tsx → /
export default function HomePage() {
return <h1>หน้าแรก</h1>
}
// src/app/about/page.tsx → /about
export default function AboutPage() {
return <h1>เกี่ยวกับเรา</h1>
}
// src/app/contact/page.tsx → /contact
export default function ContactPage() {
return <h1>ติดต่อเรา</h1>
}
Dynamic Routes
// src/app/blog/[slug]/page.tsx → /blog/:slug
// เช่น /blog/nextjs-guide, /blog/react-tutorial
interface PageProps {
params: { slug: string }
}
export default async function BlogPost({ params }: PageProps) {
const post = await getPostBySlug(params.slug)
return (
<article>
<h1>{post.title}</h1>
<div dangerouslySetInnerHTML={{ __html: post.content }} />
</article>
)
}
// สร้าง Static Paths สำหรับ SSG
export async function generateStaticParams() {
const posts = await getAllPosts()
return posts.map((post) => ({ slug: post.slug }))
}
Catch-all Routes และ Route Groups
// [...slug]/page.tsx → จับทุก path (/docs/a, /docs/a/b, /docs/a/b/c)
// [[...slug]]/page.tsx → Optional catch-all (/ ก็ match)
// Route Groups — จัดกลุ่มโดยไม่สร้าง URL segment
// src/app/(marketing)/about/page.tsx → /about
// src/app/(marketing)/pricing/page.tsx → /pricing
// src/app/(dashboard)/settings/page.tsx → /settings
// Parallel Routes — แสดงหลาย page พร้อมกัน
// src/app/@modal/login/page.tsx
// src/app/@sidebar/page.tsx
Server Components vs Client Components
นี่คือ Concept ที่สำคัญที่สุดใน Next.js 14 App Router ทำความเข้าใจให้ดีเพราะจะกระทบการออกแบบ Component ทั้งหมด
Server Components (Default)
ทุก Component ใน App Router เป็น Server Component โดย Default หมายความว่า Component จะ Render บน Server เท่านั้น ไม่ส่ง JavaScript ไปที่ Browser
// Server Component (default — ไม่ต้องเขียน 'use server')
// สามารถ:
// - ดึงข้อมูลจาก Database โดยตรง
// - อ่านไฟล์บน Server
// - ใช้ Secret Keys / Environment Variables
// - ลด Bundle Size (ไม่ส่ง JS ไป Client)
// src/app/users/page.tsx
import { db } from '@/lib/db'
export default async function UsersPage() {
// Query database โดยตรง — ไม่ต้องสร้าง API!
const users = await db.user.findMany()
return (
<div>
<h1>Users ({users.length})</h1>
<ul>
{users.map(user => (
<li key={user.id}>{user.name}</li>
))}
</ul>
</div>
)
}
Client Components
ใช้ "use client" directive เมื่อต้องการ Interactivity
// Client Component — ต้องเขียน 'use client' บรรทัดแรก
// ใช้เมื่อต้องการ:
// - useState, useEffect, useRef
// - Event handlers (onClick, onChange, etc.)
// - Browser APIs (localStorage, window, etc.)
// - Third-party libs ที่ต้องการ Browser
'use client'
import { useState } from 'react'
export default function Counter() {
const [count, setCount] = useState(0)
return (
<div>
<p>Count: {count}</p>
<button onClick={() => setCount(count + 1)}>
เพิ่ม
</button>
</div>
)
}
Layouts และ Templates
Layout เป็น UI ที่ครอบหลายหน้า เช่น Navbar, Sidebar, Footer ใน Next.js ใช้ไฟล์ layout.tsx
// src/app/layout.tsx — Root Layout (บังคับ)
import type { Metadata } from 'next'
import './globals.css'
export const metadata: Metadata = {
title: { default: 'My App', template: '%s | My App' },
description: 'Full-Stack Next.js Application',
}
export default function RootLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<html lang="th">
<body>
<nav>Navbar ที่แสดงทุกหน้า</nav>
<main>{children}</main>
<footer>Footer ที่แสดงทุกหน้า</footer>
</body>
</html>
)
}
// src/app/dashboard/layout.tsx — Nested Layout
export default function DashboardLayout({
children,
}: {
children: React.ReactNode
}) {
return (
<div className="flex">
<aside>Sidebar เฉพาะหน้า Dashboard</aside>
<div className="flex-1">{children}</div>
</div>
)
}
ข้อสำคัญ: Layout จะไม่ Re-render เมื่อเปลี่ยนหน้าภายใน Segment เดียวกัน ทำให้ Navigation เร็วมาก ถ้าต้องการ Re-render ทุกครั้ง ใช้ template.tsx แทน
Loading UI และ Error Handling
// src/app/blog/loading.tsx — แสดงขณะ Loading (ใช้ React Suspense)
export default function Loading() {
return (
<div className="animate-pulse">
<div className="h-8 bg-gray-200 rounded w-3/4 mb-4"></div>
<div className="h-4 bg-gray-200 rounded w-full mb-2"></div>
<div className="h-4 bg-gray-200 rounded w-5/6"></div>
</div>
)
}
// src/app/blog/error.tsx — แสดงเมื่อเกิด Error (Error Boundary)
'use client' // Error components ต้องเป็น Client Component
export default function Error({
error,
reset,
}: {
error: Error & { digest?: string }
reset: () => void
}) {
return (
<div>
<h2>เกิดข้อผิดพลาด!</h2>
<p>{error.message}</p>
<button onClick={() => reset()}>ลองใหม่</button>
</div>
)
}
// src/app/not-found.tsx — หน้า 404 Custom
export default function NotFound() {
return (
<div>
<h1>404 — ไม่พบหน้านี้</h1>
<p>ขออภัย หน้าที่คุณต้องการไม่มีอยู่ในระบบ</p>
</div>
)
}
Data Fetching ใน Next.js 14
การดึงข้อมูลใน App Router ง่ายมาก ใช้ async/await ใน Server Component ได้เลย
Server-side Data Fetching
// Fetch data ใน Server Component
export default async function PostsPage() {
// fetch() ถูก extend ด้วย Next.js ให้มี caching
const res = await fetch('https://api.example.com/posts', {
// Cache options:
cache: 'force-cache', // SSG (default) — cache ตลอด
// cache: 'no-store', // SSR — ดึงใหม่ทุก request
// next: { revalidate: 60 } // ISR — cache 60 วินาที
})
const posts = await res.json()
return (
<ul>
{posts.map((post: any) => (
<li key={post.id}>{post.title}</li>
))}
</ul>
)
}
Server Actions — Full-Stack ไม่ต้องสร้าง API
// src/app/actions.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
await db.post.create({
data: { title, content }
})
revalidatePath('/blog')
}
export async function deletePost(id: string) {
await db.post.delete({ where: { id } })
revalidatePath('/blog')
}
// ใช้ใน Component
// src/app/blog/new/page.tsx
import { createPost } from '@/app/actions'
export default function NewPostPage() {
return (
<form action={createPost}>
<input name="title" placeholder="หัวข้อ" required />
<textarea name="content" placeholder="เนื้อหา" required />
<button type="submit">สร้างบทความ</button>
</form>
)
}
API Routes (Route Handlers)
สำหรับกรณีที่ต้องการ REST API แบบดั้งเดิม Next.js มี Route Handlers ให้ใช้
// src/app/api/posts/route.ts
import { NextRequest, NextResponse } from 'next/server'
import { db } from '@/lib/db'
// GET /api/posts
export async function GET(request: NextRequest) {
const searchParams = request.nextUrl.searchParams
const page = parseInt(searchParams.get('page') || '1')
const limit = 10
const posts = await db.post.findMany({
skip: (page - 1) * limit,
take: limit,
orderBy: { createdAt: 'desc' }
})
return NextResponse.json({ data: posts, page })
}
// POST /api/posts
export async function POST(request: NextRequest) {
const body = await request.json()
const post = await db.post.create({
data: { title: body.title, content: body.content }
})
return NextResponse.json(post, { status: 201 })
}
// src/app/api/posts/[id]/route.ts
// GET /api/posts/:id
export async function GET(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const post = await db.post.findUnique({ where: { id: params.id } })
if (!post) return NextResponse.json({ error: 'Not Found' }, { status: 404 })
return NextResponse.json(post)
}
// PUT /api/posts/:id
export async function PUT(
request: NextRequest,
{ params }: { params: { id: string } }
) {
const body = await request.json()
const post = await db.post.update({
where: { id: params.id },
data: body
})
return NextResponse.json(post)
}
// DELETE /api/posts/:id
export async function DELETE(
request: NextRequest,
{ params }: { params: { id: string } }
) {
await db.post.delete({ where: { id: params.id } })
return NextResponse.json({ success: true })
}
Middleware — ดัก Request ก่อนถึงหน้า
// src/middleware.ts (ต้องอยู่ที่ root ของ src/)
import { NextResponse } from 'next/server'
import type { NextRequest } from 'next/server'
export function middleware(request: NextRequest) {
// ตรวจ Authentication
const token = request.cookies.get('session')?.value
if (!token && request.nextUrl.pathname.startsWith('/dashboard')) {
// Redirect ไปหน้า Login
return NextResponse.redirect(new URL('/login', request.url))
}
// เพิ่ม Custom Header
const response = NextResponse.next()
response.headers.set('x-pathname', request.nextUrl.pathname)
return response
}
// กำหนดว่า Middleware ทำงานกับ Path ไหนบ้าง
export const config = {
matcher: ['/dashboard/:path*', '/api/admin/:path*']
}
Metadata และ SEO
// Static Metadata — กำหนดตายตัว
import type { Metadata } from 'next'
export const metadata: Metadata = {
title: 'หน้าแรก | My App',
description: 'เว็บแอป Next.js สำหรับ...',
keywords: ['next.js', 'react', 'web development'],
openGraph: {
title: 'My App',
description: 'เว็บแอป Next.js',
url: 'https://myapp.com',
siteName: 'My App',
images: [{ url: 'https://myapp.com/og.jpg', width: 1200, height: 630 }],
type: 'website',
},
twitter: { card: 'summary_large_image' },
robots: { index: true, follow: true },
}
// Dynamic Metadata — ดึงจาก Database
export async function generateMetadata({ params }: { params: { slug: string } }): Promise<Metadata> {
const post = await getPostBySlug(params.slug)
return {
title: post.title,
description: post.excerpt,
openGraph: {
title: post.title,
description: post.excerpt,
images: [post.coverImage],
},
}
}
Authentication Patterns
Next.js สามารถทำ Authentication ได้หลายวิธี วิธีที่นิยมที่สุดคือใช้ NextAuth.js (Auth.js)
// ติดตั้ง
// npm install next-auth@beta
// src/auth.ts
import NextAuth from 'next-auth'
import Credentials from 'next-auth/providers/credentials'
import GitHub from 'next-auth/providers/github'
import Google from 'next-auth/providers/google'
export const { handlers, signIn, signOut, auth } = NextAuth({
providers: [
GitHub({
clientId: process.env.GITHUB_ID,
clientSecret: process.env.GITHUB_SECRET,
}),
Google({
clientId: process.env.GOOGLE_ID,
clientSecret: process.env.GOOGLE_SECRET,
}),
Credentials({
credentials: {
email: { label: 'Email', type: 'email' },
password: { label: 'Password', type: 'password' },
},
async authorize(credentials) {
// ตรวจสอบ credentials กับ database
const user = await db.user.findUnique({
where: { email: credentials.email as string }
})
if (!user) return null
const valid = await bcrypt.compare(
credentials.password as string, user.hashedPassword
)
return valid ? user : null
},
}),
],
pages: {
signIn: '/login',
},
})
// src/app/api/auth/[...nextauth]/route.ts
import { handlers } from '@/auth'
export const { GET, POST } = handlers
// ใช้ใน Server Component
import { auth } from '@/auth'
export default async function DashboardPage() {
const session = await auth()
if (!session) redirect('/login')
return <h1>Welcome, {session.user?.name}</h1>
}
Database Integration — Prisma ORM
Prisma เป็น ORM ยอดนิยมสำหรับ Next.js ทำให้ทำงานกับ Database ง่ายและ Type-safe
# ติดตั้ง Prisma
npm install prisma @prisma/client
npx prisma init
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql" // หรือ mysql, sqlite
url = env("DATABASE_URL")
}
model User {
id String @id @default(cuid())
email String @unique
name String?
posts Post[]
createdAt DateTime @default(now())
}
model Post {
id String @id @default(cuid())
title String
content String?
published Boolean @default(false)
author User @relation(fields: [authorId], references: [id])
authorId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
# Migration
npx prisma migrate dev --name init
npx prisma generate # สร้าง Client
// src/lib/db.ts — Singleton Prisma Client
import { PrismaClient } from '@prisma/client'
const globalForPrisma = globalThis as unknown as { prisma: PrismaClient }
export const db = globalForPrisma.prisma || new PrismaClient()
if (process.env.NODE_ENV !== 'production') globalForPrisma.prisma = db
Drizzle ORM — ทางเลือกที่เร็วกว่า
// npm install drizzle-orm pg
// npm install -D drizzle-kit
// src/db/schema.ts
import { pgTable, text, timestamp, boolean } from 'drizzle-orm/pg-core'
export const posts = pgTable('posts', {
id: text('id').primaryKey(),
title: text('title').notNull(),
content: text('content'),
published: boolean('published').default(false),
createdAt: timestamp('created_at').defaultNow(),
})
// ใช้ Drizzle
import { db } from '@/db'
import { posts } from '@/db/schema'
import { eq } from 'drizzle-orm'
const allPosts = await db.select().from(posts).where(eq(posts.published, true))
const newPost = await db.insert(posts).values({ id: 'xxx', title: 'Hello' }).returning()
ISR และ On-Demand Revalidation
Incremental Static Regeneration (ISR) ช่วยให้หน้า Static สามารถอัพเดทได้โดยไม่ต้อง Re-build ทั้งเว็บ
// Time-based Revalidation — อัพเดททุก N วินาที
export default async function BlogPage() {
const posts = await fetch('https://api.example.com/posts', {
next: { revalidate: 3600 } // Re-fetch ทุก 1 ชั่วโมง
})
// ...
}
// หรือกำหนดระดับ Page
export const revalidate = 3600 // Revalidate ทุก 1 ชั่วโมง
// On-Demand Revalidation — อัพเดทเมื่อเรียก
// src/app/api/revalidate/route.ts
import { revalidatePath, revalidateTag } from 'next/cache'
import { NextRequest } from 'next/server'
export async function POST(request: NextRequest) {
const secret = request.headers.get('x-revalidate-secret')
if (secret !== process.env.REVALIDATE_SECRET) {
return Response.json({ error: 'Unauthorized' }, { status: 401 })
}
const { path, tag } = await request.json()
if (tag) {
revalidateTag(tag)
} else if (path) {
revalidatePath(path)
}
return Response.json({ revalidated: true })
}
Image Optimization ด้วย next/image
import Image from 'next/image'
// next/image ทำสิ่งเหล่านี้อัตโนมัติ:
// - Lazy loading (โหลดเมื่อเลื่อนถึง)
// - Resize ตามขนาดหน้าจอ
// - แปลงเป็น WebP/AVIF
// - ป้องกัน Layout Shift (CLS)
export default function Gallery() {
return (
<div>
{/* รูปจาก Local */}
<Image
src="/images/hero.jpg"
alt="Hero Image"
width={1200}
height={600}
priority // โหลดทันที (สำหรับ above-the-fold)
/>
{/* รูปจาก External URL */}
<Image
src="https://cdn.example.com/photo.jpg"
alt="External Photo"
width={400}
height={300}
sizes="(max-width: 768px) 100vw, 400px"
/>
{/* รูป Fill container */}
<div style={{ position: 'relative', width: '100%', height: '400px' }}>
<Image
src="/images/cover.jpg"
alt="Cover"
fill
style={{ objectFit: 'cover' }}
/>
</div>
</div>
)
}
// next.config.js — อนุญาต External Images
/** @type {import('next').NextConfig} */
const nextConfig = {
images: {
remotePatterns: [
{
protocol: 'https',
hostname: 'cdn.example.com',
},
{
protocol: 'https',
hostname: '**.amazonaws.com',
},
],
},
}
module.exports = nextConfig
Internationalization (i18n)
// Next.js 14 App Router ใช้ Sub-path Routing สำหรับ i18n
// /th/about, /en/about
// src/app/[lang]/layout.tsx
export default function LangLayout({
children, params
}: {
children: React.ReactNode
params: { lang: string }
}) {
return <html lang={params.lang}><body>{children}</body></html>
}
// src/lib/dictionary.ts
const dictionaries = {
th: () => import('@/dictionaries/th.json').then(m => m.default),
en: () => import('@/dictionaries/en.json').then(m => m.default),
}
export const getDictionary = async (lang: 'th' | 'en') =>
dictionaries[lang]()
// ใช้ใน Page
export default async function AboutPage({ params }: { params: { lang: string } }) {
const dict = await getDictionary(params.lang as 'th' | 'en')
return <h1>{dict.about.title}</h1>
}
// Middleware สำหรับ Language Detection
// src/middleware.ts
import { match } from '@formatjs/intl-localematcher'
import Negotiator from 'negotiator'
const locales = ['th', 'en']
const defaultLocale = 'th'
function getLocale(request: NextRequest) {
const headers = { 'accept-language': request.headers.get('accept-language') || '' }
const languages = new Negotiator({ headers }).languages()
return match(languages, locales, defaultLocale)
}
Next.js vs Remix vs Astro
| คุณสมบัติ | Next.js | Remix | Astro |
|---|---|---|---|
| ประเภท | Full-Stack React | Full-Stack React | Content-focused MPA |
| Rendering | SSR, SSG, ISR, CSR | SSR เป็นหลัก | SSG เป็นหลัก |
| Data Fetching | Server Components, Server Actions | Loaders, Actions | Astro.props fetch |
| Bundle Size | ปานกลาง | ปานกลาง | เล็กมาก (zero JS default) |
| UI Library | React เท่านั้น | React เท่านั้น | React, Vue, Svelte, etc. |
| เหมาะกับ | App ทุกประเภท | App ที่เน้น Form/Data | Blog, Docs, Marketing |
| Hosting | Vercel, Self-host | ทุก Node.js hosting | Static hosting, Vercel |
| Community | ใหญ่ที่สุด | กำลังเติบโต | เติบโตเร็ว |
Deployment และ Production
Deploy บน Vercel (แนะนำ)
# ง่ายที่สุด — Push ไป GitHub แล้ว Connect กับ Vercel
# 1. สร้าง Account ที่ vercel.com
# 2. Import GitHub Repository
# 3. Vercel จะ Deploy อัตโนมัติทุกครั้งที่ Push
# หรือใช้ CLI
npm install -g vercel
vercel # Deploy to preview
vercel --prod # Deploy to production
Self-hosted ด้วย Docker
# Dockerfile
FROM node:20-alpine AS base
# Dependencies
FROM base AS deps
WORKDIR /app
COPY package.json package-lock.json ./
RUN npm ci
# Build
FROM base AS builder
WORKDIR /app
COPY --from=deps /app/node_modules ./node_modules
COPY . .
RUN npx prisma generate
RUN npm run build
# Production
FROM base AS runner
WORKDIR /app
ENV NODE_ENV=production
RUN addgroup --system --gid 1001 nodejs
RUN adduser --system --uid 1001 nextjs
COPY --from=builder /app/public ./public
COPY --from=builder /app/.next/standalone ./
COPY --from=builder /app/.next/static ./.next/static
USER nextjs
EXPOSE 3000
CMD ["node", "server.js"]
// next.config.js — เปิด Standalone mode สำหรับ Docker
const nextConfig = {
output: 'standalone',
}
Static Export
// next.config.js — Export เป็น Static HTML
const nextConfig = {
output: 'export',
// หมายเหตุ: ไม่สามารถใช้ Server-side features
// (API Routes, SSR, ISR, Middleware) ได้
}
Performance Optimization
เทคนิคที่ควรใช้
// 1. Dynamic Import — โหลด Component เมื่อต้องการ
import dynamic from 'next/dynamic'
const HeavyChart = dynamic(() => import('@/components/Chart'), {
loading: () => <p>Loading chart...</p>,
ssr: false, // ไม่ Render บน Server
})
// 2. Parallel Data Fetching — ดึงข้อมูลพร้อมกัน
async function DashboardPage() {
// แย่: Sequential (ช้า)
// const users = await getUsers()
// const posts = await getPosts()
// ดี: Parallel (เร็ว)
const [users, posts] = await Promise.all([
getUsers(),
getPosts(),
])
return <Dashboard users={users} posts={posts} />
}
// 3. Route Segment Config
export const dynamic = 'force-static' // หรือ 'force-dynamic', 'auto'
export const revalidate = 3600
export const fetchCache = 'force-cache'
// 4. Prefetch Links
import Link from 'next/link'
// Next.js จะ Prefetch หน้าที่ Link ชี้ไปอัตโนมัติ
<Link href="/about" prefetch={true}>About</Link>
// 5. Font Optimization
import { Noto_Sans_Thai } from 'next/font/google'
const notoSansThai = Noto_Sans_Thai({
subsets: ['thai'],
display: 'swap',
weight: ['300', '400', '500', '600', '700'],
})
// ใช้: <body className={notoSansThai.className}>
Bundle Analyzer
# ติดตั้ง
npm install @next/bundle-analyzer
// next.config.js
const withBundleAnalyzer = require('@next/bundle-analyzer')({
enabled: process.env.ANALYZE === 'true',
})
module.exports = withBundleAnalyzer(nextConfig)
# วิเคราะห์ Bundle
ANALYZE=true npm run build
Testing ใน Next.js
// ติดตั้ง Testing Tools
// npm install -D jest @testing-library/react @testing-library/jest-dom
// npm install -D @types/jest jest-environment-jsdom
// jest.config.ts
import type { Config } from 'jest'
import nextJest from 'next/jest'
const createJestConfig = nextJest({ dir: './' })
const config: Config = {
coverageProvider: 'v8',
testEnvironment: 'jsdom',
setupFilesAfterSetup: ['<rootDir>/jest.setup.ts'],
}
export default createJestConfig(config)
// __tests__/components/Counter.test.tsx
import { render, screen, fireEvent } from '@testing-library/react'
import Counter from '@/components/Counter'
describe('Counter', () => {
it('should increment count', () => {
render(<Counter />)
const button = screen.getByText('เพิ่ม')
fireEvent.click(button)
expect(screen.getByText('Count: 1')).toBeInTheDocument()
})
})
// E2E Testing ด้วย Playwright
// npm install -D @playwright/test
// npx playwright test
Project จริง: Blog Application
มาสร้าง Blog Application จริงด้วย Next.js 14 + Prisma + Tailwind CSS กัน
// 1. Schema — prisma/schema.prisma
model Post {
id String @id @default(cuid())
title String
slug String @unique
content String
excerpt String?
published Boolean @default(false)
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
}
// 2. Server Actions — src/app/actions/posts.ts
'use server'
import { db } from '@/lib/db'
import { revalidatePath } from 'next/cache'
import { redirect } from 'next/navigation'
export async function createPost(formData: FormData) {
const title = formData.get('title') as string
const content = formData.get('content') as string
const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, '-')
await db.post.create({
data: { title, slug, content, excerpt: content.slice(0, 160) }
})
revalidatePath('/blog')
redirect('/blog')
}
// 3. Blog List — src/app/blog/page.tsx
export default async function BlogPage() {
const posts = await db.post.findMany({
where: { published: true },
orderBy: { createdAt: 'desc' },
})
return (
<div className="grid gap-6">
{posts.map(post => (
<article key={post.id} className="border p-4 rounded-lg">
<Link href={`/blog/${post.slug}`}>
<h2 className="text-xl font-bold">{post.title}</h2>
</Link>
<p className="text-gray-600">{post.excerpt}</p>
</article>
))}
</div>
)
}
// 4. Blog Detail — src/app/blog/[slug]/page.tsx
export default async function BlogDetailPage({ params }: { params: { slug: string } }) {
const post = await db.post.findUnique({ where: { slug: params.slug } })
if (!post) notFound()
return (
<article>
<h1 className="text-3xl font-bold">{post.title}</h1>
<time className="text-gray-500">{post.createdAt.toLocaleDateString('th-TH')}</time>
<div className="prose mt-6">{post.content}</div>
</article>
)
}
Best Practices สำหรับ Next.js 2026
- ใช้ Server Components เป็นหลัก: ลด JavaScript Bundle ได้มาก ใช้ Client Component เฉพาะที่จำเป็น
- ใช้ Server Actions แทน API Routes: สำหรับ Mutation (Create, Update, Delete) ลด Boilerplate
- จัดโครงสร้าง Feature-based: แยกโฟลเดอร์ตาม Feature ไม่ใช่ตาม Type (components, hooks, utils)
- ใช้ TypeScript: Type Safety ช่วยลด Bug และทำให้ Refactor ง่าย
- Optimize Images ด้วย next/image: ไม่ใช้ img tag ตรงๆ
- ใช้ Parallel Data Fetching: Promise.all() สำหรับ Query ที่ไม่ขึ้นกัน
- ตั้ง Revalidation ให้เหมาะสม: ไม่ต้อง SSR ทุกอย่าง ใช้ ISR ถ้าข้อมูลไม่เปลี่ยนบ่อย
- ใช้ Middleware สำหรับ Auth Guard: ไม่ต้องตรวจ Auth ทุก Page ซ้ำๆ
- Environment Variables: ใช้ NEXT_PUBLIC_ prefix เฉพาะตัวแปรที่ Client ต้องใช้
- Error Handling: สร้าง error.tsx และ not-found.tsx ทุก Route Segment ที่สำคัญ
สรุป
Next.js 14 เป็น Framework ที่ทรงพลังสำหรับการสร้าง Full-Stack Web Application ด้วย React ด้วย App Router, Server Components และ Server Actions คุณสามารถสร้างแอปพลิเคชันที่มีทั้ง Frontend และ Backend ใน Project เดียวกัน โดยไม่ต้องแยก API Server ออกไป
ฟีเจอร์อย่าง ISR ทำให้เว็บเร็วเหมือน Static Site แต่อัพเดทข้อมูลได้เหมือน Dynamic Site ขณะที่ Server Components ช่วยลด JavaScript ที่ส่งไป Browser ให้น้อยที่สุด ทำให้ User Experience ดีเยี่ยม
ไม่ว่าคุณจะเป็น Frontend Developer ที่อยากขยายไป Full-Stack หรือ Backend Developer ที่อยากสร้าง UI สวยๆ Next.js คือเครื่องมือที่ตอบโจทย์ทั้งสองฝั่ง เริ่มต้นง่ายด้วย npx create-next-app@latest แล้วลองสร้างโปรเจกต์แรกของคุณวันนี้
