Build a REST API with Go's Standard Library
Go developers used to reach for a router package as soon as they needed path parameters. Since Go 1.22, the standard http.ServeMux supports HTTP methods and wildcards, which covers most APIs.
The server
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
)
type Book struct {
ID int `json:"id"`
Title string `json:"title"`
Author string `json:"author"`
}
type store struct {
mu sync.RWMutex
books map[int]Book
nextID int
}
func writeJSON(w http.ResponseWriter, status int, v any) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(v)
}
func main() {
s := &store{books: map[int]Book{}, nextID: 1}
mux := http.NewServeMux()
mux.HandleFunc("GET /books", func(w http.ResponseWriter, r *http.Request) {
s.mu.RLock()
defer s.mu.RUnlock()
list := make([]Book, 0, len(s.books))
for _, b := range s.books {
list = append(list, b)
}
writeJSON(w, http.StatusOK, list)
})
mux.HandleFunc("GET /books/{id}", func(w http.ResponseWriter, r *http.Request) {
id, err := strconv.Atoi(r.PathValue("id"))
if err != nil {
writeJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid id"})
return
}
s.mu.RLock()
b, ok := s.books[id]
s.mu.RUnlock()
if !ok {
writeJSON(w, http.StatusNotFound, map[string]string{"error": "not found"})
return
}
writeJSON(w, http.StatusOK, b)
})
mux.HandleFunc("POST /books", func(w http.ResponseWriter, r *http.Request) {
var b Book
if err := json.NewDecoder(http.MaxBytesReader(w, r.Body, 1<<20)).Decode(&b); err != nil || b.Title == "" {
writeJSON(w, http.StatusUnprocessableEntity, map[string]string{"error": "title is required"})
return
}
s.mu.Lock()
b.ID = s.nextID
s.nextID++
s.books[b.ID] = b
s.mu.Unlock()
writeJSON(w, http.StatusCreated, b)
})
log.Println("listening on :8080")
log.Fatal(http.ListenAndServe(":8080", logRequests(mux)))
}
Middleware is just a function
func logRequests(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
next.ServeHTTP(w, r)
log.Printf("%s %s %s", r.Method, r.URL.Path, time.Since(start))
})
}
Production touches
- Use an
http.ServerwithReadTimeout,WriteTimeoutandIdleTimeoutset; the defaults have no timeouts. - Shut down gracefully with
server.Shutdown(ctx)on SIGTERM. - A wrong method on a known path automatically gets
405 Method Not Allowed.
Was this article helpful?
Comments
No comments yet. Have a question or a tip? Be the first to share it.