📡 บทที่ 5: Data Fetching & API Routes
เรียนรู้การดึงข้อมูลและสร้าง API endpoints ใน Next.js 15+ App Router พร้อม React 19
วัตถุประสงค์การเรียนรู้
เข้าใจการ fetch ข้อมูลใน Server Components พร้อม React 19
สามารถสร้าง API Routes ใน Next.js 15+ พร้อม async request APIs ได้
เข้าใจ Enhanced Caching strategies ใน Next.js 15 (ไม่ cache by default)
รู้จัก Error Handling และ Loading States ที่ดีขึ้น
🤔 Data Fetching ใน Next.js 15 คืออะไร?
Data Fetching หมายถึงการดึงข้อมูลจากแหล่งต่างๆ เช่น API, Database, หรือ File system โดย Next.js 15 มีการเปลี่ยนแปลงสำคัญในระบบ caching และ async request APIs ที่ทำให้การจัดการข้อมูลดีขึ้น
🎯 วิธีการ Fetch ข้อมูลใน Next.js
Server Components
ใช้ fetch โดยตรงใน async components
ใช้เมื่อ: SEO สำคัญ, ข้อมูลคงที่
Client Components
ใช้ useEffect + fetch หรือ SWR/React Query
ใช้เมื่อ: ต้องการ interactivity, real-time updates
API Routes
สร้าง backend API endpoints ใน Next.js
ใช้เมื่อ: ต้องการ API สำหรับ external หรือ complex logic
🚀 Caching Strategies
Next.js มี caching strategies หลายแบบที่ช่วยเพิ่มประสิทธิภาพ
Static Generation (ISG)
Generate ที่ build time และ cache
Server-side Rendering (SSR)
Generate ทุกครั้งที่ request
Client-side Fetching
Fetch ใน browser หลัง hydration
Incremental Static Regeneration
Re-generate static pages เมื่อมีการเปลี่ยนแปลง
🖥️ Server Components Data Fetching
ใน Next.js 13+ สามารถใช้ fetch โดยตรงใน Server Components ได้ โดยไม่ต้องใช้ getServerSideProps
ตัวอย่างการ fetch ข้อมูลพื้นฐานใน Server Component
// app/products/page.tsx async function ProductsPage() { // Fetch ข้อมูลบนเซิร์ฟเวอร์ const response = await fetch('https://jsonplaceholder.typicode.com/posts'); const posts = await response.json(); return ( <div> <h1>รายการโพสต์</h1> {posts.slice(0, 10).map((post: any) => ( <div key={post.id} style={{ border: '1px solid #ccc', padding: '15px', margin: '10px 0' }}> <h3>{post.title}</h3> <p>{post.body}</p> </div> ))} </div> ); } export default ProductsPage;
🚀 API Routes
API Routes ใน Next.js 13+ ใช้ App Router และอยู่ในโฟลเดอร์ app/api/
การสร้าง API endpoint ด้วย route.ts
// app/api/hello/route.ts import { NextRequest, NextResponse } from 'next/server'; // GET request export async function GET() { return NextResponse.json({ message: 'Hello from API!', timestamp: new Date().toISOString() }); } // POST request export async function POST(request: NextRequest) { const body = await request.json(); return NextResponse.json({ message: 'Data received!', data: body, timestamp: new Date().toISOString() }); } // PUT request export async function PUT(request: NextRequest) { const body = await request.json(); return NextResponse.json({ message: 'Data updated!', data: body }); } // DELETE request export async function DELETE() { return NextResponse.json({ message: 'Data deleted!' }); }
การสร้าง API routes ที่รับพารามิเตอร์
// app/api/products/[id]/route.ts import { NextRequest, NextResponse } from 'next/server'; // ข้อมูลจำลอง const products = [ { id: '1', name: 'iPhone 15', price: 35000 }, { id: '2', name: 'Samsung Galaxy S24', price: 30000 }, { id: '3', name: 'Google Pixel 8', price: 25000 } ]; export async function GET( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const product = products.find(p => p.id === id); if (!product) { return NextResponse.json( { error: 'Product not found' }, { status: 404 } ); } return NextResponse.json(product); } export async function PUT( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const body = await request.json(); const productIndex = products.findIndex(p => p.id === id); if (productIndex === -1) { return NextResponse.json( { error: 'Product not found' }, { status: 404 } ); } products[productIndex] = { ...products[productIndex], ...body }; return NextResponse.json(products[productIndex]); } export async function DELETE( request: NextRequest, { params }: { params: Promise<{ id: string }> } ) { const { id } = await params; const productIndex = products.findIndex(p => p.id === id); if (productIndex === -1) { return NextResponse.json( { error: 'Product not found' }, { status: 404 } ); } products.splice(productIndex, 1); return NextResponse.json({ message: 'Product deleted successfully' }); }
การเพิ่ม authentication, validation และ error handling
// app/api/protected/route.ts import { NextRequest, NextResponse } from 'next/server'; // Middleware สำหรับตรวจสอบ authentication function checkAuth(request: NextRequest) { const authHeader = request.headers.get('authorization'); if (!authHeader || !authHeader.startsWith('Bearer ')) { return false; } const token = authHeader.split(' ')[1]; // ตรวจสอบ token ที่นี่ (เช่น JWT verification) return token === 'valid-token'; // สำหรับตัวอย่าง } // Validation function function validateUserData(data: any) { const errors: string[] = []; if (!data.name || data.name.length < 2) { errors.push('Name must be at least 2 characters'); } if (!data.email || !data.email.includes('@')) { errors.push('Invalid email format'); } if (!data.age || data.age < 0 || data.age > 120) { errors.push('Age must be between 0 and 120'); } return errors; } export async function GET(request: NextRequest) { // ตรวจสอบ authentication if (!checkAuth(request)) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } return NextResponse.json({ message: 'This is protected data', data: ['secret1', 'secret2', 'secret3'] }); } export async function POST(request: NextRequest) { try { // ตรวจสอบ authentication if (!checkAuth(request)) { return NextResponse.json( { error: 'Unauthorized' }, { status: 401 } ); } const body = await request.json(); // Validate ข้อมูล const validationErrors = validateUserData(body); if (validationErrors.length > 0) { return NextResponse.json( { error: 'Validation failed', details: validationErrors }, { status: 400 } ); } // สมมติว่าบันทึกข้อมูลลงฐานข้อมูล const newUser = { id: Date.now().toString(), ...body, createdAt: new Date().toISOString() }; return NextResponse.json(newUser, { status: 201 }); } catch (error) { console.error('API Error:', error); return NextResponse.json( { error: 'Internal server error' }, { status: 500 } ); } }
การเชื่อมต่อกับ external APIs และ proxy requests
// app/api/weather/route.ts import { NextRequest, NextResponse } from 'next/server'; const WEATHER_API_KEY = process.env.WEATHER_API_KEY; export async function GET(request: NextRequest) { try { // รับพารามิเตอร์จาก URL const { searchParams } = new URL(request.url); const city = searchParams.get('city'); if (!city) { return NextResponse.json( { error: 'City parameter is required' }, { status: 400 } ); } // เรียก external API const response = await fetch( `https://api.openweathermap.org/data/2.5/weather?q=${city}&appid=${WEATHER_API_KEY}&units=metric`, { next: { revalidate: 300 } // Cache เป็นเวลา 5 นาที } ); if (!response.ok) { if (response.status === 404) { return NextResponse.json( { error: 'City not found' }, { status: 404 } ); } throw new Error(`Weather API error: ${response.status}`); } const weatherData = await response.json(); // ปรับแต่งข้อมูลก่อนส่งกลับ const formattedData = { city: weatherData.name, country: weatherData.sys.country, temperature: weatherData.main.temp, description: weatherData.weather[0].description, humidity: weatherData.main.humidity, windSpeed: weatherData.wind.speed, timestamp: new Date().toISOString() }; return NextResponse.json(formattedData); } catch (error) { console.error('Weather API Error:', error); return NextResponse.json( { error: 'Failed to fetch weather data' }, { status: 500 } ); } }
💻 Client Components Data Fetching
การ fetch ข้อมูลใน Client Components สำหรับ interactive features
วิธีพื้นฐานในการ fetch ข้อมูลใน Client Components
// app/components/PostsList.tsx 'use client'; import { useState, useEffect } from 'react'; interface Post { id: number; title: string; body: string; userId: number; } export default function PostsList() { const [posts, setPosts] = useState<Post[]>([]); const [loading, setLoading] = useState(true); const [error, setError] = useState<string | null>(null); useEffect(() => { const fetchPosts = async () => { try { setLoading(true); const response = await fetch('https://jsonplaceholder.typicode.com/posts'); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const data = await response.json(); setPosts(data.slice(0, 10)); // แสดงแค่ 10 posts } catch (err) { setError(err instanceof Error ? err.message : 'An error occurred'); } finally { setLoading(false); } }; fetchPosts(); }, []); if (loading) { return ( <div> <h2>กำลังโหลด...</h2> <div>โปรดรอสักครู่</div> </div> ); } if (error) { return ( <div> <h2>เกิดข้อผิดพลาด</h2> <p>{error}</p> <button onClick={() => window.location.reload()}> ลองใหม่ </button> </div> ); } return ( <div> <h2>รายการโพสต์ ({posts.length} รายการ)</h2> {posts.map(post => ( <div key={post.id} style={{ border: '1px solid #ddd', padding: '15px', margin: '10px 0', borderRadius: '5px' }}> <h3>{post.title}</h3> <p>{post.body}</p> <small>โดย User #{post.userId}</small> </div> ))} </div> ); }
การสร้าง CRUD operations ใน Client Components
// app/components/TodoManager.tsx 'use client'; import { useState, useEffect } from 'react'; interface Todo { id: number; title: string; completed: boolean; } export default function TodoManager() { const [todos, setTodos] = useState<Todo[]>([]); const [newTodo, setNewTodo] = useState(''); const [loading, setLoading] = useState(false); // Fetch todos useEffect(() => { fetchTodos(); }, []); const fetchTodos = async () => { try { const response = await fetch('https://jsonplaceholder.typicode.com/todos?_limit=5'); const data = await response.json(); setTodos(data); } catch (error) { console.error('Error fetching todos:', error); } }; // Create todo const handleAddTodo = async (e: React.FormEvent) => { e.preventDefault(); if (!newTodo.trim()) return; setLoading(true); try { const response = await fetch('https://jsonplaceholder.typicode.com/todos', { method: 'POST', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ title: newTodo, completed: false, userId: 1, }), }); const newTodoData = await response.json(); // เพิ่ม ID ที่ไม่ซ้ำสำหรับ demo newTodoData.id = Date.now(); setTodos([newTodoData, ...todos]); setNewTodo(''); } catch (error) { console.error('Error adding todo:', error); } finally { setLoading(false); } }; // Update todo const handleToggleTodo = async (id: number) => { const todo = todos.find(t => t.id === id); if (!todo) return; try { const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, { method: 'PUT', headers: { 'Content-Type': 'application/json', }, body: JSON.stringify({ ...todo, completed: !todo.completed, }), }); if (response.ok) { setTodos(todos.map(t => t.id === id ? { ...t, completed: !t.completed } : t )); } } catch (error) { console.error('Error updating todo:', error); } }; // Delete todo const handleDeleteTodo = async (id: number) => { try { const response = await fetch(`https://jsonplaceholder.typicode.com/todos/${id}`, { method: 'DELETE', }); if (response.ok) { setTodos(todos.filter(t => t.id !== id)); } } catch (error) { console.error('Error deleting todo:', error); } }; return ( <div> <h2>Todo Manager</h2> {/* Add new todo form */} <form onSubmit={handleAddTodo} style={{ marginBottom: '20px' }}> <input type="text" value={newTodo} onChange={(e) => setNewTodo(e.target.value)} placeholder="เพิ่ม todo ใหม่..." style={{ padding: '8px', marginRight: '10px', width: '300px', border: '1px solid #ddd', borderRadius: '4px' }} /> <button type="submit" disabled={loading} style={{ padding: '8px 16px', backgroundColor: '#007bff', color: 'white', border: 'none', borderRadius: '4px', cursor: loading ? 'not-allowed' : 'pointer' }} > {loading ? 'กำลังเพิ่ม...' : 'เพิ่ม'} </button> </form> {/* Todos list */} <div> {todos.map(todo => ( <div key={todo.id} style={{ display: 'flex', alignItems: 'center', padding: '10px', border: '1px solid #eee', margin: '5px 0', borderRadius: '4px' }} > <input type="checkbox" checked={todo.completed} onChange={() => handleToggleTodo(todo.id)} style={{ marginRight: '10px' }} /> <span style={{ flex: 1, textDecoration: todo.completed ? 'line-through' : 'none', color: todo.completed ? '#888' : '#000' }} > {todo.title} </span> <button onClick={() => handleDeleteTodo(todo.id)} style={{ backgroundColor: '#dc3545', color: 'white', border: 'none', padding: '4px 8px', borderRadius: '4px', cursor: 'pointer' }} > ลบ </button> </div> ))} </div> </div> ); }
✋ ฝึกปฏิบัติ: สร้าง Data Fetching System
ลองสร้าง system ที่ใช้ทั้ง Server Components, API Routes และ Client Components
สร้าง API Route
สร้าง API endpoint สำหรับจัดการข้อมูลผู้ใช้
$ mkdir -p app/api/users
$ touch app/api/users/route.ts
// app/api/users/route.ts import { NextRequest, NextResponse } from 'next/server'; // Mock database let users = [ { id: 1, name: 'สมชาย', email: 'somchai@example.com', age: 25 }, { id: 2, name: 'สมหญิง', email: 'somying@example.com', age: 30 }, { id: 3, name: 'สมศักดิ์', email: 'somsak@example.com', age: 35 } ]; export async function GET() { return NextResponse.json({ users, count: users.length, timestamp: new Date().toISOString() }); } export async function POST(request: NextRequest) { try { const body = await request.json(); // Validation if (!body.name || !body.email || !body.age) { return NextResponse.json( { error: 'Name, email และ age จำเป็นต้องมี' }, { status: 400 } ); } const newUser = { id: Math.max(...users.map(u => u.id)) + 1, name: body.name, email: body.email, age: parseInt(body.age) }; users.push(newUser); return NextResponse.json(newUser, { status: 201 }); } catch (error) { return NextResponse.json( { error: 'Invalid JSON' }, { status: 400 } ); } }
สร้าง Server Component
สร้าง Client Component
⚡ Performance Tips
🚀 การใช้ Cache อย่างมีประสิทธิภาพ
// Static data - cache ตลอดไป const staticData = await fetch('https://api.example.com/config', { cache: 'force-cache' }); // Dynamic data - ไม่ cache const liveData = await fetch('https://api.example.com/live', { cache: 'no-store' }); // Time-based revalidation const newsData = await fetch('https://api.example.com/news', { next: { revalidate: 3600 } // 1 ชั่วโมง }); // Tag-based revalidation const productData = await fetch('https://api.example.com/products', { next: { tags: ['products'] } });
💡 Best Practices สำคัญ
ใช้ Server Components สำหรับ initial data loading เพื่อ SEO และ performance ที่ดี
ใช้ API Routes เพื่อซ่อน sensitive data เช่น API keys, database credentials
ใช้ appropriate caching strategies: force-cache สำหรับ static data, revalidate สำหรับ time-based updates
เสมอต้องมี error handling: ใช้ try-catch, error boundaries และ fallback UIs
ใช้ TypeScript interfaces สำหรับ data types เพื่อ type safety และ better developer experience
ใช้ loading states และ Suspense เพื่อ user experience ที่ดี
🎯 ยินดีด้วย! คุณเรียนจบบทที่ 5 แล้ว
เยี่ยมมาก! ตอนนี้คุณเข้าใจ Data Fetching และ API Routes ใน Next.js 15 แล้ว พร้อมสำหรับการเรียนรู้เรื่อง Layouts และ Templates ในบทถัดไป