Resource Handlers

Map GET, POST, PUT and DELETE onto one resource with ServeMux patterns, and keep each handler thin.

Syntaxmux.HandleFunc("GET /notes/{id}", h) // then id := r.PathValue("id")

A resource is one kind of thing your API exposes: notes, users, orders. REST maps HTTP methods onto that resource instead of inventing a URL per action. GET reads, POST creates, PUT replaces, PATCH changes part of it, DELETE removes it. The path names the thing; the method says what to do to it.

Routing one resource

Since Go 1.22, a ServeMux pattern may begin with a method and contain a wildcard, a name in braces that matches one path segment: "GET /notes/{id}". Inside the handler, r.PathValue("id") returns what that segment matched. If a request's path matches but its method does not, the mux answers 405 Method Not Allowed by itself and sets an Allow header.

PatternDoesReplies
GET /noteslist a page200
POST /notescreate201 plus Location
GET /notes/{id}read one200 or 404
PUT /notes/{id}replace200 or 404
DELETE /notes/{id}remove204 or 404

Thin handlers

A handler does five things: decode the body, check the shape, call one service method, turn any error into a status code, encode the reply. mux.HandleFunc(p, f) is shorthand for mux.Handle(p, http.HandlerFunc(f)) — the same registration, written two ways.

What trips people up

POST is not safe to retry: a client that times out and sends again creates two notes. PUT and DELETE are idempotent, meaning the same request twice leaves the same end state, so pick one answer for deleting something already gone and keep it.

Example

Example · go
package main

import (
	"encoding/json"
	"fmt"
	"net/http"
	"net/http/httptest"
	"strings"
)

type Note struct {
	ID   string `json:"id"`
	Text string `json:"text"`
}

// A map stands in for a database here. Wiring a real store is the next lesson.
var notes = map[string]Note{}

func createNote(w http.ResponseWriter, r *http.Request) {
	var n Note
	if err := json.NewDecoder(r.Body).Decode(&n); err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}
	n.ID = "1" // a real store assigns this
	notes[n.ID] = n
	w.Header().Set("Location", "/notes/"+n.ID)
	w.WriteHeader(http.StatusCreated)
}

func getNote(w http.ResponseWriter, r *http.Request) {
	n, ok := notes[r.PathValue("id")]
	if !ok {
		http.Error(w, "not found", http.StatusNotFound)
		return
	}
	body, err := json.Marshal(n)
	if err != nil {
		http.Error(w, "internal error", http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	if _, err := w.Write(body); err != nil {
		return
	}
}

func main() {
	mux := http.NewServeMux()
	mux.HandleFunc("POST /notes", createNote)
	mux.HandleFunc("GET /notes/{id}", getNote)

	// httptest drives the mux in memory, so no port is opened.
	rec := httptest.NewRecorder()
	body := strings.NewReader(`{"text":"buy milk"}`)
	mux.ServeHTTP(rec, httptest.NewRequest(http.MethodPost, "/notes", body))
	fmt.Println(rec.Code, rec.Header().Get("Location"))

	rec = httptest.NewRecorder()
	mux.ServeHTTP(rec, httptest.NewRequest(http.MethodGet, "/notes/1", nil))
	fmt.Printf("%d %s\n", rec.Code, rec.Body.String())
}

// Output:
// 201 /notes/1
// 200 {"id":"1","text":"buy milk"}

When to use it

  • Exposing a notes table as /notes and /notes/{id} with the right status code for each method.
  • Returning 201 with a Location header so a client can fetch what it just created.
  • Replacing a third-party router with net/http now that method patterns exist.

More examples

Every route for one resource in one place

Reading this function tells you the whole public surface of the resource. Both {id} and {$} arrived with Go 1.22. A PATCH request to /notes/1 gets 405 and an Allow header listing GET, PUT and DELETE, from the mux, with no code of yours involved. Curl it with: curl -i -X DELETE localhost:8080/notes/1

Example · go
// routes is the only place that knows the URL shape of the API.
func routes(a api) http.Handler {
	mux := http.NewServeMux()
	mux.HandleFunc("GET /notes", a.listNotes)
	mux.HandleFunc("POST /notes", a.createNote)
	mux.HandleFunc("GET /notes/{id}", a.getNote)
	mux.HandleFunc("PUT /notes/{id}", a.replaceNote)
	mux.HandleFunc("DELETE /notes/{id}", a.deleteNote)
	// {$} matches only the exact path "/", so this route cannot swallow
	// requests to /notes. Without {$}, "/" would match everything.
	mux.HandleFunc("GET /{$}", a.index)
	return mux
}

A thin create handler

Nine lines of HTTP and one call into the service. The rules about what text is acceptable live in Create, so the same check applies to any caller. Passing r.Context() means the service stops working if the client hangs up mid-request.

Example · go
func (a api) createNote(w http.ResponseWriter, r *http.Request) {
	// A separate input struct means a client cannot set fields it must not
	// control, such as the id or an owner.
	var in struct {
		Text string `json:"text"`
	}
	dec := json.NewDecoder(r.Body)
	// Reject "txt" instead of silently ignoring the typo.
	dec.DisallowUnknownFields()
	if err := dec.Decode(&in); err != nil {
		http.Error(w, "invalid json", http.StatusBadRequest)
		return
	}

	n, err := a.svc.Create(r.Context(), in.Text)
	if err != nil {
		writeError(w, err) // one place maps errors to status codes
		return
	}

	w.Header().Set("Location", "/notes/"+n.ID)
	w.WriteHeader(http.StatusCreated)
}

HandleFunc and its longhand

These two lines do exactly the same thing, so use one or the other: registering the same pattern twice makes the mux panic. You need the longhand when you already hold an http.Handler rather than a function, which is what middleware and http.StripPrefix hand you.

Example · go
func health(w http.ResponseWriter, r *http.Request) {
	if _, err := w.Write([]byte("ok")); err != nil {
		return
	}
}

mux := http.NewServeMux()

// Shorthand.
mux.HandleFunc("GET /health", health)

// Longhand: identical. http.HandlerFunc is a type whose ServeHTTP method
// calls the function, which is how a plain func becomes an http.Handler.
mux.Handle("GET /health", http.HandlerFunc(health))

Discussion

  • Be the first to comment on this lesson.