Update & Delete Handlers
The last two lessons covered reading (listTasks, getTask) and creating
(createTask). updateTask and deleteTask close out basic CRUD, and both
lean on ground already covered: decodeJSON from Chapter 1, the comma-ok
existence check from getTask, and TaskStore.Update/TaskStore.Delete
from Part 1. Nothing here is new machinery — it's the same pieces, assembled
one more time.
updateTask: a full-record replace
func (s *server) updateTask(w http.ResponseWriter, r *http.Request) {
var in struct {
Title string `json:"title"`
Done bool `json:"done"`
Priority int `json:"priority"`
}
if !decodeJSON(w, r, &in) {
return
}
t, ok := s.store.Update(r.PathValue("id"), in.Title, in.Done, in.Priority)
if !ok {
writeError(w, http.StatusNotFound, "task not found")
return
}
writeJSON(w, http.StatusOK, t)
}
Compare the decode target to createTask's: this one adds Done. That's the
defining trait of PUT — it's a replacement, not a patch. A client sends
the entire desired state of the resource, and the handler overwrites the
existing task with exactly that state. TaskStore.Update reflects this
directly: it takes title, done, and priority all at once and builds a
brand-new Task value from them, rather than mutating individual fields.
There's no way to send just {"done": true} and leave title and
priority untouched — a client that wants to flip one field still has to
resend the whole record. That limitation is deliberate, not an oversight;
Chapter 4 introduces PATCH for partial updates, and the contrast will be
clearer once both exist side by side.
The status handling should feel familiar: TaskStore.Update already returns
(Task, bool), the same comma-ok shape Get uses, so updateTask translates
it the exact same way getTask did — ok false means 404, ok true means
200 with the updated task in the body. That's the store's existing-check
bool doing its job a second time. It was built once, back when Part 1
covered Update and Delete on TaskStore directly, and every handler that
touches a specific task by id gets a working 404 check for free as a result.
deleteTask: success with nothing to say
func (s *server) deleteTask(w http.ResponseWriter, r *http.Request) {
if !s.store.Delete(r.PathValue("id")) {
writeError(w, http.StatusNotFound, "task not found")
return
}
w.WriteHeader(http.StatusNoContent)
}
TaskStore.Delete returns a single bool: true if a task existed and was
removed, false if there was nothing to delete. deleteTask uses that
exactly the way updateTask uses Update's ok — a 404 on a miss. The
interesting part is what happens on success.
Every handler so far has ended in writeJSON, because every handler so far
had something to report back — a task, a list of tasks. deleteTask doesn't:
the task is gone, and there's nothing meaningful left to describe. HTTP has a
status code for exactly this situation, 204 No Content, which means "the
request succeeded, and there is deliberately no body." That's a different
claim from 200 with an empty body — 204 tells the client not to expect
one at all.
Because there's no body, there's no call to writeJSON. writeJSON always
writes a Content-Type header and encodes a value; a 204 response should
do neither. Instead, deleteTask calls w.WriteHeader(http.StatusNoContent)
directly — the same low-level method writeJSON calls internally, just
without the header-and-encode steps wrapped around it. Skip that and call
writeJSON(w, http.StatusNoContent, nil) instead, and encoding/json will
happily encode nil as the four bytes null, which is a body — exactly what
204 promises the client it won't get. The distinction is subtle enough to
miss without a test catching it, which is why the hidden test for this lesson
checks the response body is empty, not just that the status code is right.
What's left
With listTasks, getTask, createTask, updateTask, and deleteTask all
in place, the store's five methods each have a handler translating them into
HTTP. Chapter 4 picks up from here: validating request bodies before they
reach the store, and adding PATCH for the partial updates PUT can't
express.
Practice
1. Update & Delete Handlers
IntermediateTwo more handlers finish basic CRUD.
(s *server) updateTask(w, r)— decode{"title","done","priority"}, callstore.Updatewith the id fromr.PathValue("id"). On a hit, respond200with the updated task; on a miss,404.(s *server) deleteTask(w, r)— callstore.Delete. On success, respond204 No Content(no body!). On a miss,404.
204means "done, nothing to send back" — usew.WriteHeader(http.StatusNoContent)directly and write no body.package main import ( "encoding/json" "net/http" "strconv" "sync" ) type Task struct { ID string `json:"id"` Title string `json:"title"` Done bool `json:"done"` Priority int `json:"priority"` } // Store is the persistence seam the HTTP layer depends on. *TaskStore // satisfies it; a database-backed store could too, without touching handlers. type Store interface { Create(title string, priority int) Task Get(id string) (Task, bool) List() []Task Update(id, title string, done bool, priority int) (Task, bool) Delete(id string) bool } type TaskStore struct { mu sync.Mutex items map[string]Task order []string nextID int } func NewTaskStore() *TaskStore { return &TaskStore{items: make(map[string]Task), nextID: 1} } func (s *TaskStore) Create(title string, priority int) Task { s.mu.Lock() defer s.mu.Unlock() id := strconv.Itoa(s.nextID) s.nextID++ t := Task{ID: id, Title: title, Done: false, Priority: priority} s.items[id] = t s.order = append(s.order, id) return t } func (s *TaskStore) Get(id string) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() t, ok := s.items[id] return t, ok } func (s *TaskStore) List() []Task { s.mu.Lock() defer s.mu.Unlock() out := make([]Task, 0, len(s.order)) for _, id := range s.order { out = append(out, s.items[id]) } return out } func (s *TaskStore) Update(id, title string, done bool, priority int) (Task, bool) { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.items[id]; !ok { return Task{}, false } t := Task{ID: id, Title: title, Done: done, Priority: priority} s.items[id] = t return t, true } func (s *TaskStore) Delete(id string) bool { s.mu.Lock() defer s.mu.Unlock() if _, ok := s.items[id]; !ok { return false } delete(s.items, id) for i, oid := range s.order { if oid == id { s.order = append(s.order[:i], s.order[i+1:]...) break } } return true } func writeJSON(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(v) } func writeError(w http.ResponseWriter, status int, msg string) { writeJSON(w, status, map[string]string{"error": msg}) } func decodeJSON(w http.ResponseWriter, r *http.Request, dst any) bool { dec := json.NewDecoder(r.Body) dec.DisallowUnknownFields() if err := dec.Decode(dst); err != nil { writeError(w, http.StatusBadRequest, "invalid request body") return false } return true } type server struct { store Store } func (s *server) updateTask(w http.ResponseWriter, r *http.Request) { // TODO: decode {"title","done","priority"} from the request body, call // s.store.Update with r.PathValue("id"), and respond 200 on a hit or // 404 on a miss. } func (s *server) deleteTask(w http.ResponseWriter, r *http.Request) { // TODO: call s.store.Delete with r.PathValue("id") and respond 204 (no // body) on success or 404 on a miss. } func main() {}Sign in to write and run your solution in the browser.
Sign in for hints and the solution.