Build a REST API with Go's Standard Library

A
Author
09 Aug, 2026 1 min read 577 views

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.Server with ReadTimeout, WriteTimeout and IdleTimeout set; 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.

Leave a comment

Comments are reviewed before they appear.