# Todo — reference implementation (Go + Chi + SQLite) This is a complete example. Generate equivalent structure for the given project. Use ONLY the fields from the JSON spec — do not add extras. ## go.mod Chi v5 router, modernc.org/sqlite (pure Go, no CGO). ``` module todo-go go 1.23.0 toolchain go1.23.12 require ( github.com/go-chi/chi/v5 v5.2.1 modernc.org/sqlite v1.37.1 ) require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/mattn/go-isatty v0.0.20 // indirect github.com/ncruces/go-strftime v0.1.9 // indirect github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect golang.org/x/sys v0.33.0 // indirect modernc.org/libc v1.65.7 // indirect modernc.org/mathutil v1.7.1 // indirect modernc.org/memory v1.11.0 // indirect ) ``` ## models.go Data structs: Todo (full row), CreateTodo (POST), UpdateTodo (PUT, all fields optional pointers). ```go package main // Todo represents a task with priority and status tracking. type Todo struct { ID int64 `json:"id"` Title string `json:"title"` Description *string `json:"description,omitempty"` DueDate *string `json:"due_date,omitempty"` Priority int64 `json:"priority"` Status string `json:"status"` } // CreateTodo is the request body for creating a new todo. type CreateTodo struct { Title string `json:"title"` Description *string `json:"description,omitempty"` DueDate *string `json:"due_date,omitempty"` Priority *int64 `json:"priority,omitempty"` Status *string `json:"status,omitempty"` } // UpdateTodo is the request body for updating an existing todo. type UpdateTodo struct { Title *string `json:"title,omitempty"` Description *string `json:"description,omitempty"` DueDate *string `json:"due_date,omitempty"` Priority *int64 `json:"priority,omitempty"` Status *string `json:"status,omitempty"` } ``` ## handlers.go CRUD handlers as closures taking *sql.DB. Key patterns: INSERT RETURNING, sql.ErrNoRows for 404, RowsAffected for delete. ```go package main import ( "database/sql" "encoding/json" "net/http" "strconv" "github.com/go-chi/chi/v5" ) // POST — decode JSON, defaults with nil-check, INSERT RETURNING, StatusCreated. func createTodo(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { var input CreateTodo if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, err.Error(), http.StatusBadRequest); return } priority := int64(1) if input.Priority != nil { priority = *input.Priority } status := "pending" if input.Status != nil { status = *input.Status } var todo Todo err := db.QueryRow( `INSERT INTO todos (title, description, due_date, priority, status) VALUES (?, ?, ?, ?, ?) RETURNING id, title, description, due_date, priority, status`, input.Title, input.Description, input.DueDate, priority, status, ).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return } w.Header().Set("Content-Type", "application/json") w.WriteHeader(http.StatusCreated) json.NewEncoder(w).Encode(todo) } } // GET list — db.Query + rows.Scan loop, empty slice not nil. func listTodos(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { rows, err := db.Query("SELECT id, title, description, due_date, priority, status FROM todos") if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return } defer rows.Close() todos := []Todo{} for rows.Next() { var t Todo rows.Scan(&t.ID, &t.Title, &t.Description, &t.DueDate, &t.Priority, &t.Status) todos = append(todos, t) } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(todos) } } // GET by id — QueryRow + sql.ErrNoRows → 404. func getTodo(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) var todo Todo err := db.QueryRow( "SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id, ).Scan(&todo.ID, &todo.Title, &todo.Description, &todo.DueDate, &todo.Priority, &todo.Status) if err == sql.ErrNoRows { http.Error(w, "not found", http.StatusNotFound); return } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(todo) } } // PUT — fetch existing, merge with input nil-checks, UPDATE RETURNING. func updateTodo(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) var existing Todo err := db.QueryRow("SELECT id, title, description, due_date, priority, status FROM todos WHERE id = ?", id, ).Scan(&existing.ID, &existing.Title, &existing.Description, &existing.DueDate, &existing.Priority, &existing.Status) if err == sql.ErrNoRows { http.Error(w, "not found", http.StatusNotFound); return } if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return } var input UpdateTodo if err := json.NewDecoder(r.Body).Decode(&input); err != nil { http.Error(w, err.Error(), http.StatusBadRequest); return } if input.Title != nil { existing.Title = *input.Title } if input.Description != nil { existing.Description = input.Description } if input.DueDate != nil { existing.DueDate = input.DueDate } if input.Priority != nil { existing.Priority = *input.Priority } if input.Status != nil { existing.Status = *input.Status } var updated Todo err = db.QueryRow( `UPDATE todos SET title=?, description=?, due_date=?, priority=?, status=? WHERE id=? RETURNING id, title, description, due_date, priority, status`, existing.Title, existing.Description, existing.DueDate, existing.Priority, existing.Status, id, ).Scan(&updated.ID, &updated.Title, &updated.Description, &updated.DueDate, &updated.Priority, &updated.Status) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return } w.Header().Set("Content-Type", "application/json") json.NewEncoder(w).Encode(updated) } } // DELETE — Exec + RowsAffected == 0 → 404, else 204. func deleteTodo(db *sql.DB) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { id, _ := strconv.ParseInt(chi.URLParam(r, "id"), 10, 64) result, err := db.Exec("DELETE FROM todos WHERE id = ?", id) if err != nil { http.Error(w, err.Error(), http.StatusInternalServerError); return } rows, _ := result.RowsAffected() if rows == 0 { http.Error(w, "not found", http.StatusNotFound); return } w.WriteHeader(http.StatusNoContent) } } ``` ## main.go Entry point: SQLite connection, table init, Chi router on port 3000. ```go package main import ( "database/sql" "fmt" "log" "net/http" "github.com/go-chi/chi/v5" _ "modernc.org/sqlite" ) // InitDB creates tables if they don't exist. func InitDB(db *sql.DB) { _, err := db.Exec(`CREATE TABLE IF NOT EXISTS todos ( id INTEGER PRIMARY KEY AUTOINCREMENT, title TEXT NOT NULL, description TEXT, due_date TEXT, priority INTEGER NOT NULL DEFAULT 1, status TEXT NOT NULL DEFAULT 'pending' )`) if err != nil { log.Fatal(err) } } // NewRouter creates a chi router with all routes. func NewRouter(db *sql.DB) http.Handler { r := chi.NewRouter() r.Post("/todos", createTodo(db)) r.Get("/todos", listTodos(db)) r.Get("/todos/{id}", getTodo(db)) r.Put("/todos/{id}", updateTodo(db)) r.Delete("/todos/{id}", deleteTodo(db)) return r } func main() { db, err := sql.Open("sqlite", "file:app.db?mode=rwc") if err != nil { log.Fatal(err) } defer db.Close() InitDB(db) fmt.Println("Server running: http://127.0.0.1:3000") log.Fatal(http.ListenAndServe("127.0.0.1:3000", NewRouter(db))) } ``` ## handlers_test.go Integration tests: setupTestServer with httptest.NewServer + :memory: SQLite, unique data per test. ```go package main import ( "database/sql" "encoding/json" "fmt" "net/http" "net/http/httptest" "strings" "testing" _ "modernc.org/sqlite" ) func setupTestServer(t *testing.T) (*httptest.Server, *sql.DB) { t.Helper() db, err := sql.Open("sqlite", ":memory:") if err != nil { t.Fatal(err) } InitDB(db) return httptest.NewServer(NewRouter(db)), db } func TestCreateTodo(t *testing.T) { ts, db := setupTestServer(t) defer ts.Close() defer db.Close() resp, err := http.Post(ts.URL+"/todos", "application/json", strings.NewReader(`{"title":"Buy groceries","priority":2}`)) if err != nil { t.Fatal(err) } defer resp.Body.Close() if resp.StatusCode != http.StatusCreated { t.Fatalf("expected 201, got %d", resp.StatusCode) } var body map[string]interface{} json.NewDecoder(resp.Body).Decode(&body) if body["title"] != "Buy groceries" { t.Fatalf("expected 'Buy groceries', got %v", body["title"]) } if body["id"] == nil { t.Fatal("expected id") } } func TestGetTodoByID(t *testing.T) { ts, db := setupTestServer(t) defer ts.Close() defer db.Close() resp, _ := http.Post(ts.URL+"/todos", "application/json", strings.NewReader(`{"title":"Fetchable task"}`)) var created map[string]interface{} json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() id := created["id"].(float64) resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id)) defer resp.Body.Close() if resp.StatusCode != http.StatusOK { t.Fatalf("expected 200, got %d", resp.StatusCode) } } func TestGetTodoNotFound(t *testing.T) { ts, db := setupTestServer(t) defer ts.Close() defer db.Close() resp, _ := http.Get(ts.URL + "/todos/99999") defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404, got %d", resp.StatusCode) } } func TestDeleteTodo(t *testing.T) { ts, db := setupTestServer(t) defer ts.Close() defer db.Close() resp, _ := http.Post(ts.URL+"/todos", "application/json", strings.NewReader(`{"title":"Deletable task"}`)) var created map[string]interface{} json.NewDecoder(resp.Body).Decode(&created) resp.Body.Close() id := created["id"].(float64) req, _ := http.NewRequest(http.MethodDelete, ts.URL+"/todos/"+fmt.Sprintf("%.0f", id), nil) resp, _ = http.DefaultClient.Do(req) defer resp.Body.Close() if resp.StatusCode != http.StatusNoContent { t.Fatalf("expected 204, got %d", resp.StatusCode) } resp, _ = http.Get(ts.URL + "/todos/" + fmt.Sprintf("%.0f", id)) defer resp.Body.Close() if resp.StatusCode != http.StatusNotFound { t.Fatalf("expected 404 after delete, got %d", resp.StatusCode) } } ```