Skip to content

Use Case: Building a REST API

Since Go 1.22, net/http’s built-in router handles method matching and path parameters, so a lot of small-to-medium APIs no longer need a third-party framework at all.

package main
import (
"encoding/json"
"log"
"net/http"
)
type Task struct {
ID string `json:"id"`
Name string `json:"name"`
Done bool `json:"done"`
}
var tasks = map[string]Task{
"1": {ID: "1", Name: "Learn Go", Done: false},
}
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /tasks", listTasks)
mux.HandleFunc("GET /tasks/{id}", getTask)
mux.HandleFunc("POST /tasks", createTask)
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", mux))
}
func listTasks(w http.ResponseWriter, r *http.Request) {
list := make([]Task, 0, len(tasks))
for _, t := range tasks {
list = append(list, t)
}
writeJSON(w, http.StatusOK, list)
}
func getTask(w http.ResponseWriter, r *http.Request) {
id := r.PathValue("id")
task, ok := tasks[id]
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "task not found"})
return
}
writeJSON(w, http.StatusOK, task)
}
func createTask(w http.ResponseWriter, r *http.Request) {
var t Task
if err := json.NewDecoder(r.Body).Decode(&t); err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid JSON"})
return
}
tasks[t.ID] = t
writeJSON(w, http.StatusCreated, t)
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}

Run it with go run main.go, then:

Terminal window
curl localhost:8080/tasks
curl -X POST localhost:8080/tasks -d '{"id":"2","name":"Ship it"}'
  • Persistence: swap the map[string]Task for database/sql (with pgx or sqlite3) behind a small TaskStore interface — see Interfaces.
  • Validation & structured errors: return a consistent error shape ({"error": {"code": "...", "message": "..."}}) instead of ad-hoc strings.
  • Middleware: wrap mux with logging, recovery, and auth middleware, chained as shown in Functions.
  • Graceful shutdown: listen for SIGINT/SIGTERM and call server.Shutdown(ctx) so in-flight requests finish cleanly — this is where Context pays off directly.

A runnable version (single endpoint, in-memory)

Section titled “A runnable version (single endpoint, in-memory)”