You can build a full CRUD TODO API in Go using the standard library's net/http package by defining a struct for your data, creating a slice to act as an in-memory store, and registering four handler functions for Create, Read, Update, and Delete operations. This approach requires no external dependencies and provides a clear foundation for adding a real database later.
Here is a complete, runnable example using a single file. It defines a Todo struct, manages state in a global slice, and uses JSON encoding for the request/response body.
package main
import (
"encoding/json"
"fmt"
"net/http"
"sync"
)
type Todo struct {
ID int `json:"id"`
Title string `json:"title"`
Done bool `json:"done"`
}
var (
todos = []Todo{}
nextID = 1
mu sync.Mutex // Protects concurrent access to the slice
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/todos", handleTodos)
mux.HandleFunc("/todos/", handleSingleTodo)
fmt.Println("Server running on :8080")
http.ListenAndServe(":8080", mux)
}
func handleTodos(w http.ResponseWriter, r *http.Request) {
mu.Lock()
defer mu.Unlock()
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(todos)
case http.MethodPost:
var todo Todo
if err := json.NewDecoder(r.Body).Decode(&todo); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
todo.ID = nextID
nextID++
todos = append(todos, todo)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(todo)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
}
func handleSingleTodo(w http.ResponseWriter, r *http.Request) {
// Extract ID from URL path (e.g., /todos/1)
idStr := r.URL.Path[len("/todos/"):]
var id int
fmt.Sscanf(idStr, "%d", &id)
mu.Lock()
defer mu.Unlock()
for i, t := range todos {
if t.ID == id {
switch r.Method {
case http.MethodGet:
json.NewEncoder(w).Encode(t)
case http.MethodPut:
var updated Todo
if err := json.NewDecoder(r.Body).Decode(&updated); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
updated.ID = id // Ensure ID doesn't change
todos[i] = updated
json.NewEncoder(w).Encode(updated)
case http.MethodDelete:
todos = append(todos[:i], todos[i+1:]...)
w.WriteHeader(http.StatusNoContent)
default:
http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
}
return
}
}
http.Error(w, "Todo not found", http.StatusNotFound)
}
To test the API, you can use curl commands directly in your terminal. First, create a new task:
curl -X POST http://localhost:8080/todos \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries", "done": false}'
Next, retrieve all tasks, update the specific one by ID, and finally delete it:
# Get all
curl http://localhost:8080/todos
# Update ID 1
curl -X PUT http://localhost:8080/todos/1 \
-H "Content-Type: application/json" \
-d '{"title": "Buy groceries", "done": true}'
# Delete ID 1
curl -X DELETE http://localhost:8080/todos/1
This implementation handles concurrency safely using a sync.Mutex to prevent race conditions when multiple requests hit the server simultaneously. While this uses an in-memory slice, you can easily replace the todos slice and the locking logic with a database connection pool (like sqlx or gorm) for production use.