📡 บทที่ 5: Data Fetching & API Routes

เรียนรู้การดึงข้อมูลและสร้าง API endpoints ใน Next.js 15+ App Router พร้อม React 19
30 นาที
ปานกลาง
สำคัญมาก
Next.js 15
วัตถุประสงค์การเรียนรู้

เข้าใจการ 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 สำคัญ, ข้อมูลคงที่

ข้อดี:SEO ดีเร็วไม่มี loading state
ข้อเสีย:ไม่ interactiveไม่ real-time
Client Components

ใช้ useEffect + fetch หรือ SWR/React Query

ใช้เมื่อ: ต้องการ interactivity, real-time updates

ข้อดี:InteractiveReal-timeLoading states
ข้อเสีย:ไม่ดีต่อ SEOช้ากว่า server
API Routes

สร้าง backend API endpoints ใน Next.js

ใช้เมื่อ: ต้องการ API สำหรับ external หรือ complex logic

ข้อดี:FlexibleSecureReusable
ข้อเสีย:Extra requestซับซ้อนกว่า

🚀 Caching Strategies

Next.js มี caching strategies หลายแบบที่ช่วยเพิ่มประสิทธิภาพ

Static Generation (ISG)

Generate ที่ build time และ cache

Blog posts, Product pages
Server-side Rendering (SSR)

Generate ทุกครั้งที่ request

User dashboard, Real-time data
Client-side Fetching

Fetch ใน browser หลัง hydration

Interactive data, User actions
Incremental Static Regeneration

Re-generate static pages เมื่อมีการเปลี่ยนแปลง

E-commerce, CMS content

🖥️ 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 ในบทถัดไป