A router answers two questions: does this path name a route, and does that route support this method? Keep those questions separate. GET /missing is 404. PUT /notes is 405 with an Allow header because /notes is a real collection route.
This API’s small route table is written out. Health supports GET/HEAD, the collection supports GET/POST, and an item supports GET/DELETE. A numeric path does not prove a stored note exists; the repository decides that later.
The parser is supplied in this exercise; complete the method dispatch. Use equality, not substring containment. HTTP method names are case-sensitive.
For item IDs, conversion and validation are different. Atoi can parse 01 or +1, but this route admits only the canonical positive decimal spelling from 1 through 1,000,000. A round trip through Itoa rejects aliases.
package main
import ("fmt";"strconv")
func main(){
for _,raw:=range []string{"1","01","+1","0"}{n,err:=strconv.Atoi(raw);ok:=err==nil&&n>0&&n<=1000000&&strconv.Itoa(n)==raw;fmt.Println(raw,ok)}
}The handler passes URL.EscapedPath to the router. That preserves a spelling such as /notes/%31 instead of silently turning it into /notes/1. This is a deliberate teaching API policy; other APIs can choose a different normalization contract, but all layers must agree.
No slash cleaning, redirects or fallback page is part of this service. Paths with trailing or extra segments fail. A query string does not change the URL path. Method failures preserve an exact, stable Allow list for clients and tests.
Routing returns a decision rather than writing the response or mutating storage. This makes the table straightforward to test and keeps responsibilities visible in the final handler.
package main
import ("fmt";"net/url")
func main(){
u,_:=url.Parse("http://example.test/notes/12?view=short");fmt.Println(u.EscapedPath(),u.RawQuery)
}Complete the dispatch and test every allowed method, several wrong methods, leading zeros, encoded digits and oversized IDs. Do not query the repository from the router.
Next we define a bounded request-body schema before any note can be created.
Follow one request through the service.
These seven exchanges were generated by the downloadable Go replay against one fresh in-memory store. Choose a recorded step to inspect its request, response and resulting note count. Choosing a step does not send a live request.
- Client requestused
- Route & methodused
- Bounded decoderused
- Repositoryused
- JSON responseused
Request
POST /notes
Content-Type: application/json{
"title": " learn "
}Recorded response
{
"Content-Length": "25",
"Content-Type": "application/json; charset=utf-8",
"Location": "/notes/1",
"X-Content-Type-Options": "nosniff"
}{
"id": 1,
"title": "learn"
}Headers, status, body and stored count are actual recorder results. The highlighted stages annotate the corresponding code path; this is a replay, not a distributed trace. The duplicate-key step leaves the stored count at one.
Download the runnable service, replay and tests →