← Danh sách bài học
Bài 20/20
🚀 Bài 20: Dự Án Todo List API
🎯 Mục tiêu:
- Áp dụng tất cả kiến thức đã học
- Xây dựng REST API hoàn chỉnh
- CRUD: Create, Read, Update, Delete
- Error handling và clean code
1. Project Structure
todo-api/
├── main.go
├── go.mod
└── README.md
# Khởi tạo project
mkdir todo-api && cd todo-api
go mod init todo-api
2. Todo Model
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"strings"
"sync"
"time"
)
// Todo model
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Completed bool `json:"completed"`
CreatedAt time.Time `json:"created_at"`
}
// In-memory storage
var (
todos = make(map[int]Todo)
nextID = 1
todosMu sync.RWMutex
)
3. Helper Functions
// Response helpers
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, msg string) {
respondJSON(w, status, map[string]string{"error": msg})
}
// Parse ID from URL
func getIDFromURL(path string) (int, error) {
parts := strings.Split(path, "/")
if len(parts) < 3 {
return 0, fmt.Errorf("invalid path")
}
return strconv.Atoi(parts[2])
}
4. CRUD Handlers
// GET /todos - List all
func listTodos(w http.ResponseWriter, r *http.Request) {
todosMu.RLock()
defer todosMu.RUnlock()
list := make([]Todo, 0, len(todos))
for _, t := range todos {
list = append(list, t)
}
respondJSON(w, http.StatusOK, list)
}
// POST /todos - Create
func createTodo(w http.ResponseWriter, r *http.Request) {
var input struct {
Title string `json:"title"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "Invalid JSON")
return
}
if input.Title == "" {
respondError(w, http.StatusBadRequest, "Title required")
return
}
todosMu.Lock()
todo := Todo{
ID: nextID,
Title: input.Title,
Completed: false,
CreatedAt: time.Now(),
}
todos[nextID] = todo
nextID++
todosMu.Unlock()
respondJSON(w, http.StatusCreated, todo)
}
// GET /todos/{id} - Get one
func getTodo(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromURL(r.URL.Path)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid ID")
return
}
todosMu.RLock()
todo, exists := todos[id]
todosMu.RUnlock()
if !exists {
respondError(w, http.StatusNotFound, "Todo not found")
return
}
respondJSON(w, http.StatusOK, todo)
}
// PUT /todos/{id} - Update
func updateTodo(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromURL(r.URL.Path)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid ID")
return
}
var input struct {
Title string `json:"title"`
Completed bool `json:"completed"`
}
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
respondError(w, http.StatusBadRequest, "Invalid JSON")
return
}
todosMu.Lock()
defer todosMu.Unlock()
todo, exists := todos[id]
if !exists {
respondError(w, http.StatusNotFound, "Todo not found")
return
}
todo.Title = input.Title
todo.Completed = input.Completed
todos[id] = todo
respondJSON(w, http.StatusOK, todo)
}
// DELETE /todos/{id}
func deleteTodo(w http.ResponseWriter, r *http.Request) {
id, err := getIDFromURL(r.URL.Path)
if err != nil {
respondError(w, http.StatusBadRequest, "Invalid ID")
return
}
todosMu.Lock()
defer todosMu.Unlock()
if _, exists := todos[id]; !exists {
respondError(w, http.StatusNotFound, "Todo not found")
return
}
delete(todos, id)
respondJSON(w, http.StatusOK, map[string]string{
"message": "Deleted successfully",
})
}
5. Router & Main
func todosRouter(w http.ResponseWriter, r *http.Request) {
// /todos
if r.URL.Path == "/todos" || r.URL.Path == "/todos/" {
switch r.Method {
case "GET":
listTodos(w, r)
case "POST":
createTodo(w, r)
default:
respondError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
return
}
// /todos/{id}
switch r.Method {
case "GET":
getTodo(w, r)
case "PUT":
updateTodo(w, r)
case "DELETE":
deleteTodo(w, r)
default:
respondError(w, http.StatusMethodNotAllowed, "Method not allowed")
}
}
func main() {
http.HandleFunc("/todos", todosRouter)
http.HandleFunc("/todos/", todosRouter)
log.Println("Server running at http://localhost:8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}
6. Test API
# Create todo
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title": "Học Go"}'
# List todos
curl http://localhost:8080/todos
# Get one
curl http://localhost:8080/todos/1
# Update
curl -X PUT http://localhost:8080/todos/1 \
-H "Content-Type: application/json" \
-d '{"title": "Học Go xong!", "completed": true}'
# Delete
curl -X DELETE http://localhost:8080/todos/1
🎉 Chúc Mừng!
Bạn đã hoàn thành khóa học Golang cho người mới bắt đầu!
Bạn đã học được: Variables, Functions, Structs, Interfaces, Concurrency, HTTP Server, JSON API và nhiều hơn nữa.
📝 Bước Tiếp Theo
- 🔹 Thêm database (PostgreSQL/MongoDB)
- 🔹 Học framework: Gin, Echo, Fiber
- 🔹 Authentication với JWT
- 🔹 Docker & Deployment
- 🔹 Xây dựng microservices