package main import ( "encoding/json" "fmt" "net/http" "strconv" "strings" "sync" "sync/atomic" ) type Task struct { ID string `json:"id"` Name string `json:"name"` } type createUpdateRequest struct { Name string `json:"name"` } var ( store = make(map[string]Task) mu sync.RWMutex idSeq int64 ) func main() { mux := http.NewServeMux() mux.HandleFunc("/tasks", handleTasks) // GET list + POST create mux.HandleFunc("/tasks/", handleTaskByID) // GET/PUT/DELETE by id fmt.Println("Server running on http://localhost:8080") if err := http.ListenAndServe(":8080", mux); err != nil { panic(err) } } func handleTasks(w http.ResponseWriter, r *http.Request) { switch r.Method { case http.MethodGet: listTasks(w, r) case http.MethodPost: createTask(w, r) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } func handleTaskByID(w http.ResponseWriter, r *http.Request) { // Path: /tasks/{id} id := strings.TrimPrefix(r.URL.Path, "/tasks/") if id == "" || strings.Contains(id, "/") { writeJSONError(w, "Invalid ID", http.StatusBadRequest) return } switch r.Method { case http.MethodGet: getTask(w, r, id) case http.MethodPut: updateTask(w, r, id) case http.MethodDelete: deleteTask(w, r, id) default: http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) } } // ---------- Handlers ---------- func listTasks(w http.ResponseWriter, r *http.Request) { mu.RLock() defer mu.RUnlock() // PENTING: selalu kembalikan slice kosong, bukan nil tasks := make([]Task, 0, len(store)) for _, t := range store { tasks = append(tasks, t) } writeJSON(w, tasks, http.StatusOK) } func createTask(w http.ResponseWriter, r *http.Request) { var req createUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONError(w, "Invalid JSON body", http.StatusBadRequest) return } if strings.TrimSpace(req.Name) == "" { writeJSONError(w, "name is required and cannot be empty", http.StatusBadRequest) return } // Generate ID (server-side only) newID := strconv.FormatInt(atomic.AddInt64(&idSeq, 1), 10) task := Task{ ID: newID, Name: req.Name, } mu.Lock() store[newID] = task mu.Unlock() writeJSON(w, task, http.StatusCreated) } func getTask(w http.ResponseWriter, r *http.Request, id string) { mu.RLock() task, exists := store[id] mu.RUnlock() if !exists { writeJSONError(w, "Task not found", http.StatusNotFound) return } writeJSON(w, task, http.StatusOK) } func updateTask(w http.ResponseWriter, r *http.Request, id string) { var req createUpdateRequest if err := json.NewDecoder(r.Body).Decode(&req); err != nil { writeJSONError(w, "Invalid JSON body", http.StatusBadRequest) return } if strings.TrimSpace(req.Name) == "" { writeJSONError(w, "name is required and cannot be empty", http.StatusBadRequest) return } mu.Lock() defer mu.Unlock() task, exists := store[id] if !exists { writeJSONError(w, "Task not found", http.StatusNotFound) return } // Full replacement of name, ID remains the same task.Name = req.Name store[id] = task writeJSON(w, task, http.StatusOK) } func deleteTask(w http.ResponseWriter, r *http.Request, id string) { mu.Lock() defer mu.Unlock() if _, exists := store[id]; !exists { writeJSONError(w, "Task not found", http.StatusNotFound) return } delete(store, id) // 204 No Content — JANGAN tulis body apapun w.WriteHeader(http.StatusNoContent) } // ---------- Helper ---------- func writeJSON(w http.ResponseWriter, data interface{}, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } func writeJSONError(w http.ResponseWriter, message string, status int) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(map[string]string{ "error": message, }) }