Building a REST API
A REST API exposes resources over HTTP, and Go's standard net/http — with the 1.22 method-and-wildcard routing in http.ServeMux — gives you everything you need to route requests, read path values, decode and encode JSON, and return the right status codes.
Learn Building a REST API in our free Go course — an interactive lesson with runnable examples, a practice exercise and a quick reference.
Part of the free Go course at LearnCodingFast — hands-on lessons with examples you run in your browser, plus practice exercises and a quick quiz.
What You'll Learn in This Lesson
1️⃣ Routing by method and path value
http.NewServeMux is the standard router. Since Go 1.22 a pattern can name a method and use wildcards : GET /items/{id} matches only GETs and captures id , which you read with r.PathValue("id") . We exercise the handlers entirely with httptest so it runs with no port.
2️⃣ Creating resources: decode, validate, 201
A POST handler reads the JSON body with json.NewDecoder(r.Body).Decode(&in) , validates it, then writes a response. Set the status with w.WriteHeader(http.StatusCreated) before encoding the body — once you write bytes, the status is locked to whatever was sent.
3️⃣ Keeping handlers DRY
Every handler repeats the same three lines: set the content type, write the status, encode JSON. Factor that into a tiny writeJSON helper. Note 204 No Content for a successful DELETE — a body-less success — which never calls Encode at all.
🎯 Your Turn
Register a route and dispatch a request to it. Fill in the two blanks marked ___ , then run it.
❌ Calling w.WriteHeader after writing the body — the status is already 200; the second call logs "superfluous WriteHeader".
✅ Set headers and status before the first Write / Encode .
❌ Patterns without a method on Go < 1.22, then expecting method routing.
✅ Use Go 1.22+ and write "GET /path" ; the mux returns 405 for other methods automatically.
❌ Ignoring the Decode error — a malformed body leaves your struct half-filled.
✅ Check it and return 400 ; validate required fields too.
❌ Not setting Content-Type — clients may not treat the body as JSON.
✅ w.Header().Set("Content-Type", "application/json") before writing.
405 Method Not Allowed — the path matches but the method doesn't, and the 1.22 mux rejects it for you.
r.PathValue("id") — it returns the matched segment as a string ; convert with strconv.Atoi if you need a number.
Register GET /greet/{lang} , look up the language in the map, and write the greeting — or respond 404 for an unknown one. Then dispatch /greet/fr and /greet/de .
Practice quiz
Which http.ServeMux pattern matches only GET requests for a single item by id (Go 1.22+)?
- "/items/{id}"
- "GET /items/:id"
- "GET /items/{id}"
- "/items/(id)"
Answer: "GET /items/{id}". Since Go 1.22 a pattern can include the method and a named wildcard like {id}.
How do you read the {id} wildcard inside the handler?
- r.PathValue("id")
- r.URL.Query().Get("id")
- r.FormValue("id")
- mux.Param("id")
Answer: r.PathValue("id"). r.PathValue("id") returns the matched path segment as a string.
A request with the wrong HTTP method to a matched path returns what status automatically?
- 400 Bad Request
- 404 Not Found
- 500 Internal Server Error
- 405 Method Not Allowed
Answer: 405 Method Not Allowed. The 1.22 mux returns 405 when the path matches but the method does not.
When must you call w.WriteHeader(status)?
- After the first Write/Encode
- Before writing any body bytes
- Only inside a defer
- It is optional and ignored
Answer: Before writing any body bytes. Once you write bytes the status is locked to 200, so set it before the first write.
Which status code is conventional after a successful POST that creates a resource?
- 201 Created
- 200 OK
- 202 Accepted
- 204 No Content
Answer: 201 Created. 201 Created (http.StatusCreated) signals a new resource was created.
Which status fits a successful DELETE that returns no body?
- 200 OK
- 201 Created
- 204 No Content
- 404 Not Found
Answer: 204 No Content. 204 No Content means success with nothing to return in the body.
How do you decode a JSON request body straight from the stream?
- json.Unmarshal(r.Body, &v)
- json.NewDecoder(r.Body).Decode(&v)
- json.Decode(r.Body, &v)
- r.Body.JSON(&v)
Answer: json.NewDecoder(r.Body).Decode(&v). json.NewDecoder(r.Body).Decode(&v) reads JSON directly from the body stream.
How do you test a handler without binding a port?
- http.ListenAndServe on :0
- exec.Command("curl")
- net.Dial to localhost
- httptest.NewRecorder with httptest.NewRequest
Answer: httptest.NewRecorder with httptest.NewRequest. httptest.NewRecorder is an in-memory ResponseWriter you drive via ServeHTTP.
What does http.NewServeMux return?
- A *http.Client
- A request router (*http.ServeMux)
- An http.ResponseWriter
- A net.Listener
Answer: A request router (*http.ServeMux). http.NewServeMux creates a new request multiplexer (router).
Why set w.Header().Set("Content-Type", "application/json")?
- It is required to compile
- It selects the HTTP method
- So clients treat the body as JSON
- It enables gzip automatically
Answer: So clients treat the body as JSON. Setting the Content-Type tells clients the response body is JSON.