Say What You Mean
A REST API talks through two vocabularies. The method says what the client wants: GET read, POST create, PUT replace, PATCH change a part, DELETE remove. The status code says how it went: 2xx success, 4xx the client's mistake, 5xx the server's.
A detail worth knowing: PUT is idempotent — sending it twice ends the same as once. POST is not: two POSTs create two resources.
The Right Method
The method patterns in the mux route each verb to its own handler. Creation answers 201 Created and points at the new resource with a Location header; deletion answers 204 No Content — success with nothing to say.
GET, POST and DELETE on one resource
Three verbs, three answers: the list comes back with 200, the creation with 201 and a Location, the deletion with 204 and an empty body.
Output: Go program and captured output
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /notes", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintln(w, "notes: shopping, ideas")
})
mux.HandleFunc("POST /notes", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Location", "/notes/3")
w.WriteHeader(http.StatusCreated)
fmt.Fprintln(w, "created")
})
mux.HandleFunc("DELETE /notes/{id}", func(w http.ResponseWriter, r *http.Request) {
w.WriteHeader(http.StatusNoContent)
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{}
get, _ := http.Get(server.URL + "/notes")
body, _ := io.ReadAll(get.Body)
fmt.Println("GET /notes ->", get.StatusCode, strings.TrimSpace(string(body)))
post, _ := http.Post(server.URL+"/notes", "application/json", strings.NewReader(`{"text": "call Ben"}`))
fmt.Println("POST /notes ->", post.StatusCode, "Location:", post.Header.Get("Location"))
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/notes/3", nil)
del, _ := client.Do(req)
fmt.Println("DELETE /notes/3 ->", del.StatusCode, "(no content)")
}
The Right Status Code
Errors deserve precision too: a missing resource is 404 Not Found (with a JSON error body so machines can read it), and a wrong verb on a known path is 405 Method Not Allowed — which Go's mux answers for you when the path matches but the method does not.
200, 404 and 405
The existing note returns 200 with data, the missing one 404 with a JSON error, and the DELETE on a GET-only route is refused with 405 by the router itself.
Output: Go program and captured output
package main
import (
"fmt"
"io"
"net/http"
"net/http/httptest"
"strings"
)
func main() {
notes := map[string]string{"1": "shopping list"}
mux := http.NewServeMux()
mux.HandleFunc("GET /notes/{id}", func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
text, ok := notes[r.PathValue("id")]
if !ok {
w.WriteHeader(http.StatusNotFound)
fmt.Fprintln(w, `{"error": "note not found"}`)
return
}
fmt.Fprintf(w, `{"id": %q, "text": %q}`+"\n", r.PathValue("id"), text)
})
server := httptest.NewServer(mux)
defer server.Close()
client := &http.Client{}
for _, path := range []string{"/notes/1", "/notes/99"} {
resp, _ := http.Get(server.URL + path)
body, _ := io.ReadAll(resp.Body)
resp.Body.Close()
fmt.Println("GET ", path, "->", resp.StatusCode, strings.TrimSpace(string(body)))
}
// Wrong verb on a known path: the router answers 405 by itself.
req, _ := http.NewRequest(http.MethodDelete, server.URL+"/notes/1", nil)
resp, _ := client.Do(req)
fmt.Println("DELETE /notes/1 ->", resp.StatusCode, http.StatusText(resp.StatusCode))
}