← Danh sách bài học Bài 20/20

🚀 Bài 20: Dự Án Todo List API

⏱️ 45 phút | 📚 Tổng hợp

🎯 Mục tiêu:

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