📡 บทที่ 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 ในบทถัดไป