Capstone: JSON API server
| The spec | |
| net/http routes | |
| JSON handlers | |
| Test with httptest |
The spec
The server exposes word counting over HTTP. A GET request to the health path reports that the service runs, while a POST request to the words path accepts text and returns the top words as JSON.
The request body carries text plus an optional limit, and the response lists word and count pairs ordered from frequent to rare. Wrong methods and broken JSON answer with clear error codes.
net/http routes
A ServeMux maps paths to handler functions, and HandleFunc registers each pair in one line. The first version below answers both routes with fixed JSON, which proves the wiring before real logic lands.
ListenAndServe blocks main and serves traffic on the given port. Every handler receives a writer for the answer plus a request value with the method, path, and body.
package main
import (
"fmt"
"net/http"
)
func health(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok"}`)
}
func words(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"top":[]}`)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", health)
mux.HandleFunc("/words", words)
fmt.Println("listen on :8080")
http.ListenAndServe(":8080", mux)
}
curl localhost:8080/health
{"status":"ok"}
JSON handlers
Real handlers decode the request into a struct and encode the answer back. Struct tags bind JSON names to fields, so the wire format stays short while Go names keep their style.
The words handler rejects non POST methods, reports broken JSON with a bad request code, fills a default limit, and encodes the ranked list. The counting helpers mirror the CLI capstone from the previous lesson.
package main
import (
"encoding/json"
"fmt"
"net/http"
"sort"
"strings"
)
type countRequest struct {
Text string `json:"text"`
N int `json:"n"`
}
type wordCount struct {
Word string `json:"word"`
Count int `json:"count"`
}
func countWords(text string) map[string]int {
out := make(map[string]int)
for _, w := range strings.Fields(text) {
out[strings.ToLower(w)]++
}
return out
}
func topList(freq map[string]int, n int) []wordCount {
all := make([]wordCount, 0, len(freq))
for w, c := range freq {
all = append(all, wordCount{Word: w, Count: c})
}
sort.Slice(all, func(i, j int) bool {
if all[i].Count == all[j].Count {
return all[i].Word < all[j].Word
}
return all[i].Count > all[j].Count
})
if n > len(all) {
n = len(all)
}
return all[:n]
}
func health(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
fmt.Fprintln(w, `{"status":"ok"}`)
}
func words(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "post only", http.StatusMethodNotAllowed)
return
}
var req countRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "bad json", http.StatusBadRequest)
return
}
if req.N <= 0 {
req.N = 10
}
out := topList(countWords(req.Text), req.N)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(out)
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("/health", health)
mux.HandleFunc("/words", words)
fmt.Println("listen on :8080")
http.ListenAndServe(":8080", mux)
}
Test with httptest
Handlers are plain functions, so tests call them directly with a fake request and a recording writer. NewRequest builds the request, NewRecorder captures the answer, and the Code field holds the status.
The tests below cover the happy paths: health answers with success, and words accepts a JSON body and answers with success too. Method and decode errors deserve sibling cases in real projects.
package main
import (
"net/http"
"net/http/httptest"
"strings"
"testing"
)
func TestHealth(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, "/health", nil)
rec := httptest.NewRecorder()
health(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("code = %d", rec.Code)
}
}
func TestWords(t *testing.T) {
body := strings.NewReader(`{"text":"go go run","n":2}`)
req := httptest.NewRequest(http.MethodPost, "/words", body)
rec := httptest.NewRecorder()
words(rec, req)
if rec.Code != http.StatusOK {
t.Errorf("code = %d", rec.Code)
}
}
go test ./...
ok wordfreq 0.009s
Next: Hero roadmap: the whole course on one page
Article author: Arthur Isaev