Xây dựng dự án CRUD nhỏ (Blog/Todo App)

Tạo bởi Hoàng Vũ, chỉnh sửa cuối lúc 11 tháng 4, 2025

Trong bài này, học viên sẽ thực hành xây dựng một ứng dụng CRUD nhỏ sử dụng Next.js: có thể là Blog hoặc Todo App. Mục tiêu là giúp học viên áp dụng các kỹ năng đã học như tạo route động, gọi API nội bộ, dùng fetch để gửi request từ client, styling với Tailwind, và quản lý danh sách dữ liệu đơn giản.

Xây dựng dự án CRUD

1. Cấu trúc dự án

/pages
  /api
    /todos.js           ← API list/create
    /todos/[id].js      ← API update/delete/get
  /todos
    /index.js           ← Danh sách
    /[id].js            ← Chi tiết todo
    /create.js          ← Tạo mới
    /[id]/edit.js       ← Sửa
/components
  /TodoForm.js
  /TodoItem.js

Bạn có thể thay "todos" bằng "posts" nếu làm dạng Blog.

2. API Routes

pages/api/todos.js

let todos = [
  { id: 1, title: 'Học Next.js', done: false },
  { id: 2, title: 'Viết blog', done: true }
]

export default function handler(req, res) {
  if (req.method === 'GET') {
    res.status(200).json(todos)
  } else if (req.method === 'POST') {
    const newTodo = {
      id: Date.now(),
      title: req.body.title,
      done: false,
    }
    todos.push(newTodo)
    res.status(201).json(newTodo)
  }
}

pages/api/todos/[id].js

export default function handler(req, res) {
  const { id } = req.query
  const index = todos.findIndex(t => t.id == id)

  if (index === -1) return res.status(404).json({ message: 'Không tìm thấy' })

  if (req.method === 'GET') {
    return res.status(200).json(todos[index])
  }

  if (req.method === 'PUT') {
    todos[index] = { ...todos[index], ...req.body }
    return res.status(200).json(todos[index])
  }

  if (req.method === 'DELETE') {
    const deleted = todos.splice(index, 1)
    return res.status(200).json(deleted[0])
  }

  res.status(405).end()
}

3. Trang danh sách (/todos/index.js)

import Link from 'next/link'
import { useEffect, useState } from 'react'

export default function TodoList() {
  const [todos, setTodos] = useState([])

  useEffect(() => {
    fetch('/api/todos')
      .then(res => res.json())
      .then(setTodos)
  }, [])

  return (
    <div className="p-4">
      <h1 className="text-xl font-bold">Danh sách Todo</h1>
      <Link href="/todos/create" className="text-blue-600 underline">+ Tạo mới</Link>
      <ul className="mt-4">
        {todos.map(todo => (
          <li key={todo.id} className="border-b py-2">
            <Link href={`/todos/${todo.id}`} className="hover:underline">
              {todo.title}
            </Link>
          </li>
        ))}
      </ul>
    </div>
  )
}

4. Tạo Todo mới (/todos/create.js)

import { useState } from 'react'
import { useRouter } from 'next/router'

export default function CreateTodo() {
  const [title, setTitle] = useState('')
  const router = useRouter()

  const handleSubmit = async (e) => {
    e.preventDefault()
    await fetch('/api/todos', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ title })
    })
    router.push('/todos')
  }

  return (
    <div className="p-4">
      <h1 className="text-lg font-bold">Tạo Todo mới</h1>
      <form onSubmit={handleSubmit} className="mt-4 space-y-2">
        <input
          value={title}
          onChange={e => setTitle(e.target.value)}
          placeholder="Nhập nội dung..."
          className="border p-2 w-full"
        />
        <button className="bg-blue-600 text-white px-4 py-2 rounded">Lưu</button>
      </form>
    </div>
  )
}

5. Chi tiết + Xoá (/todos/[id].js)

import { useRouter } from 'next/router'
import { useEffect, useState } from 'react'

export default function TodoDetail() {
  const { id } = useRouter().query
  const [todo, setTodo] = useState(null)
  const router = useRouter()

  useEffect(() => {
    if (!id) return
    fetch(`/api/todos/${id}`)
      .then(res => res.json())
      .then(setTodo)
  }, [id])

  const handleDelete = async () => {
    await fetch(`/api/todos/${id}`, { method: 'DELETE' })
    router.push('/todos')
  }

  if (!todo) return <p>Loading...</p>

  return (
    <div className="p-4">
      <h1 className="text-xl font-bold">{todo.title}</h1>
      <p className="mt-2">Trạng thái: {todo.done ? 'Hoàn thành' : 'Chưa xong'}</p>
      <button onClick={handleDelete} className="mt-4 bg-red-600 text-white px-4 py-2 rounded">
        Xoá
      </button>
    </div>
  )
}

6. Styling với Tailwind

Toàn bộ giao diện sử dụng các tiện ích Tailwind để giữ giao diện gọn gàng, dễ đọc và responsive. Bạn có thể mở rộng thêm theme hoặc custom class nếu muốn.

Kết luận

Sau bài học này, học viên đã thực hành:

  • Thiết kế API routes phục vụ CRUD (GET, POST, PUT, DELETE)
  • Tạo các trang giao diện cho danh sách, chi tiết, thêm, sửa, xoá
  • Gọi API bằng fetch và cập nhật UI theo trạng thái
  • Styling hiệu quả bằng Tailwind CSS
  • Sử dụng dynamic routing trong Next.js

Dự án CRUD nhỏ này là nền tảng để xây dựng các dự án thực tế lớn hơn như blog cá nhân, trình quản lý công việc, hoặc dashboard người dùng có đăng nhập.

Website Logo

Với hơn 10 năm kinh nghiệm lập trình web và từng làm việc với nhiều framework, ngôn ngữ như PHP, JavaScript, React, jQuery, CSS, HTML, CakePHP, Laravel..., tôi hy vọng những kiến thức được chia sẻ tại đây sẽ hữu ích và thiết thực cho các bạn.

Bình luận

Website Logo

Chào, tôi là Vũ. Đây là blog hướng dẫn lập trình của tôi.

Liên hệ công việc qua email dưới đây.

lhvuctu@gmail.com

Chúng Tôi Trên

Bạn đang muốn học về lập trình website?

Bạn cần nâng cao kiến thức chuyên nghiệp hơn để nâng cao cơ hội nghề nghiệp? Liên hệ