DEV Community

Tossapol Ritcharoenwattu
Tossapol Ritcharoenwattu

Posted on

สร้าง todolist api ด้วยภาษา Go

เราจะออกแบบโครงสร้างให้มี api สำหรับงาน todolist ประเภท CRUD และให้มี web ui ที่สวยงามโดย โครงสร้าง project จะเป็นแบบนี้ครับ
Image description

File : main.go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"

    "github.com/gorilla/mux"
)

// Todo struct (Model)
// นี่คือโครงสร้างข้อมูลของแต่ละ task
type Todo struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

// ตัวแปรสำหรับเก็บข้อมูลทั้งหมด (เปรียบเสมือน Database ชั่วคราว)
var (
    todos  []Todo
    nextID = 1
    lock   = sync.Mutex{} // ใช้ lock เพื่อป้องกันการเขียนข้อมูลพร้อมกัน (thread-safe)
)

// GetTodos: Handler สำหรับดึงข้อมูลทั้งหมด (Read)
func GetTodos(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    lock.Lock()
    defer lock.Unlock()
    json.NewEncoder(w).Encode(todos)
}

// CreateTodo: Handler สำหรับสร้าง task ใหม่ (Create)
func CreateTodo(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    var todo Todo
    // อ่านข้อมูล JSON จาก request body
    if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
        http.Error(w, err.Error(), http.StatusBadRequest)
        return
    }

    lock.Lock()
    defer lock.Unlock()

    todo.ID = nextID
    nextID++
    todos = append(todos, todo)

    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(todo)
}

// UpdateTodo: Handler สำหรับอัปเดตสถานะ task (Update)
func UpdateTodo(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    id, err := strconv.Atoi(params["id"])
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    lock.Lock()
    defer lock.Unlock()

    for i, item := range todos {
        if item.ID == id {
            todos[i].Completed = !todos[i].Completed // สลับค่า Completed
            json.NewEncoder(w).Encode(todos[i])
            return
        }
    }

    http.NotFound(w, r)
}

// DeleteTodo: Handler สำหรับลบ task (Delete)
func DeleteTodo(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("Content-Type", "application/json")
    params := mux.Vars(r)
    id, err := strconv.Atoi(params["id"])
    if err != nil {
        http.Error(w, "Invalid ID", http.StatusBadRequest)
        return
    }

    lock.Lock()
    defer lock.Unlock()

    for i, item := range todos {
        if item.ID == id {
            // ---- ↓ บรรทัดที่แก้ไข ↓ ----
            todos = append(todos[:i], todos[i+1:]...)
            // ---- ↑ บรรทัดที่แก้ไข ↑ ----
            w.WriteHeader(http.StatusNoContent)
            return
        }
    }

    http.NotFound(w, r)
}

func main() {
    // สร้างข้อมูลเริ่มต้น
    todos = append(todos, Todo{ID: nextID, Title: "เรียนรู้ภาษา Go", Completed: false}); nextID++
    todos = append(todos, Todo{ID: nextID, Title: "สร้าง Web API", Completed: false}); nextID++

    // สร้าง Router
    r := mux.NewRouter()

    // API Routes
    api := r.PathPrefix("/api").Subrouter()
    api.HandleFunc("/todos", GetTodos).Methods("GET")
    api.HandleFunc("/todos", CreateTodo).Methods("POST")
    api.HandleFunc("/todos/{id}", UpdateTodo).Methods("PUT")
    api.HandleFunc("/todos/{id}", DeleteTodo).Methods("DELETE")

    // Static File Server สำหรับหน้าเว็บ UI
    // ให้ Go server ของเราสามารถเสิร์ฟไฟล์ html, css, js ได้
    r.PathPrefix("/").Handler(http.FileServer(http.Dir("./static/")))

    log.Println("Starting server on :8080")
    // เริ่มการทำงานของ Server ที่ Port 8080
    if err := http.ListenAndServe(":8080", r); err != nil {
        log.Fatal(err)
    }
}
Enter fullscreen mode Exit fullscreen mode

File : index.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Go Todo List</title>
    <link rel="stylesheet" href="style.css">
    <link href="https://fonts.googleapis.com/css2?family=Poppins:wght@300;400;600&display=swap" rel="stylesheet">
</head>
<body>
    <div class="container">
        <header>
            <h1>📝 Go Todo List</h1>
            <p>A simple CRUD application built with Golang & Vanilla JS</p>
        </header>
        <form id="todo-form">
            <input type="text" id="todo-input" placeholder="What needs to be done?" autocomplete="off" required>
            <button type="submit">Add Task</button>
        </form>
        <ul id="todo-list">
            </ul>
    </div>
    <script src="script.js"></script>
</body>
</html>
Enter fullscreen mode Exit fullscreen mode

File : style.css

:root {
    --bg-color: #1a1a2e;
    --primary-color: #16213e;
    --secondary-color: #0f3460;
    --accent-color: #e94560;
    --text-color: #dcdcdc;
    --completed-color: #555;
}

* {
    margin: 0;
    padding: 0;
    box-sizing: border-box;
}

body {
    font-family: 'Poppins', sans-serif;
    background-color: var(--bg-color);
    color: var(--text-color);
    display: flex;
    justify-content: center;
    align-items: flex-start;
    min-height: 100vh;
    padding-top: 50px;
}

.container {
    width: 100%;
    max-width: 600px;
    background-color: var(--primary-color);
    border-radius: 15px;
    padding: 30px;
    box-shadow: 0 10px 30px rgba(0, 0, 0, 0.3);
}

header {
    text-align: center;
    margin-bottom: 30px;
    border-bottom: 1px solid var(--secondary-color);
    padding-bottom: 20px;
}

header h1 {
    color: var(--accent-color);
    margin-bottom: 5px;
}

#todo-form {
    display: flex;
    margin-bottom: 30px;
}

#todo-input {
    flex-grow: 1;
    padding: 15px;
    border: 2px solid var(--secondary-color);
    border-radius: 8px 0 0 8px;
    background-color: var(--bg-color);
    color: var(--text-color);
    font-size: 16px;
    outline: none;
}

#todo-input:focus {
    border-color: var(--accent-color);
}

#todo-form button {
    padding: 15px 20px;
    border: none;
    background-color: var(--accent-color);
    color: white;
    font-size: 16px;
    font-weight: 600;
    border-radius: 0 8px 8px 0;
    cursor: pointer;
    transition: background-color 0.3s;
}

#todo-form button:hover {
    background-color: #ff5a79;
}

#todo-list {
    list-style: none;
}

#todo-list li {
    display: flex;
    align-items: center;
    background-color: var(--secondary-color);
    padding: 15px;
    border-radius: 8px;
    margin-bottom: 10px;
    transition: all 0.3s;
}

#todo-list li.completed {
    background-color: var(--bg-color);
    opacity: 0.6;
}

#todo-list li.completed span {
    text-decoration: line-through;
    color: var(--completed-color);
}

#todo-list li span {
    flex-grow: 1;
    cursor: pointer;
}

#todo-list li button {
    background: none;
    border: none;
    color: var(--text-color);
    font-size: 20px;
    cursor: pointer;
    margin-left: 10px;
    transition: color 0.3s;
}

#todo-list li button.delete:hover {
    color: var(--accent-color);
}
Enter fullscreen mode Exit fullscreen mode

File : script.js

// รอให้ HTML โหลดเสร็จก่อนเริ่มทำงาน
document.addEventListener('DOMContentLoaded', () => {
    const todoForm = document.getElementById('todo-form');
    const todoInput = document.getElementById('todo-input');
    const todoList = document.getElementById('todo-list');

    const apiBaseUrl = '/api/todos';

    // ฟังก์ชันสำหรับดึงข้อมูล Todos ทั้งหมดจาก API (Read)
    async function fetchTodos() {
        try {
            const response = await fetch(apiBaseUrl);
            const todos = await response.json();
            todoList.innerHTML = ''; // ล้างรายการเก่า
            if (todos) {
                todos.forEach(todo => renderTodo(todo));
            }
        } catch (error) {
            console.error('Failed to fetch todos:', error);
        }
    }

    // ฟังก์ชันสำหรับแสดงผล Todo 1 รายการบนหน้าเว็บ
    function renderTodo(todo) {
        const li = document.createElement('li');
        li.dataset.id = todo.id;
        if (todo.completed) {
            li.classList.add('completed');
        }

        const span = document.createElement('span');
        span.textContent = todo.title;
        // Event Listener สำหรับการกดที่ข้อความเพื่อสลับสถานะ (Update)
        span.addEventListener('click', () => toggleTodo(todo.id));

        const deleteButton = document.createElement('button');
        deleteButton.textContent = '🗑️';
        deleteButton.classList.add('delete');
        // Event Listener สำหรับปุ่มลบ (Delete)
        deleteButton.addEventListener('click', () => deleteTodo(todo.id));

        li.appendChild(span);
        li.appendChild(deleteButton);
        todoList.appendChild(li);
    }

    // Event Listener สำหรับการเพิ่ม Todo ใหม่ (Create)
    todoForm.addEventListener('submit', async (e) => {
        e.preventDefault();
        const title = todoInput.value.trim();
        if (!title) return;

        try {
            const response = await fetch(apiBaseUrl, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({ title: title, completed: false })
            });
            const newTodo = await response.json();
            renderTodo(newTodo);
            todoInput.value = ''; // เคลียร์ช่อง input
        } catch (error) {
            console.error('Failed to create todo:', error);
        }
    });

    // ฟังก์ชันสำหรับสลับสถานะ completed (Update)
    async function toggleTodo(id) {
        try {
            const response = await fetch(`${apiBaseUrl}/${id}`, {
                method: 'PUT'
            });
            const updatedTodo = await response.json();
            // อัปเดต UI โดยไม่ต้องโหลดใหม่ทั้งหมด
            const li = document.querySelector(`li[data-id='${id}']`);
            if (li) {
                li.classList.toggle('completed', updatedTodo.completed);
                li.querySelector('span').style.textDecoration = updatedTodo.completed ? 'line-through' : 'none';
            }
        } catch (error) {
            console.error('Failed to update todo:', error);
        }
    }

    // ฟังก์ชันสำหรับลบ Todo (Delete)
    async function deleteTodo(id) {
        try {
            await fetch(`${apiBaseUrl}/${id}`, {
                method: 'DELETE'
            });
            // ลบออกจาก UI
            const li = document.querySelector(`li[data-id='${id}']`);
            if (li) {
                li.remove();
            }
        } catch (error) {
            console.error('Failed to delete todo:', error);
        }
    }

    // เริ่มต้นโดยการดึงข้อมูลทั้งหมดมาแสดง
    fetchTodos();
});
Enter fullscreen mode Exit fullscreen mode

วิธีการรันโปรเจกต์
ติดตั้ง Go: ตรวจสอบให้แน่ใจว่าคุณได้ติดตั้ง Go บนเครื่องของคุณแล้ว
ติดตั้ง gorilla/mux: เปิด Terminal หรือ Command Prompt ขึ้นมาในโฟลเดอร์โปรเจกต์ (go-todolist) แล้วรันคำสั่ง:

go mod init go-todolist
go get -u github.com/gorilla/mux

รันเซิร์ฟเวอร์: รันคำสั่งต่อไปนี้เพื่อเริ่มการทำงานของ API Server
go run main.go
คุณจะเห็นข้อความ: Starting server on :8080

เปิดใช้งาน: เปิดเว็บเบราว์เซอร์แล้วเข้าไปที่ URL:
http://localhost:8080

Image description

Top comments (0)