On this page

HTTP and REST APIs

14 min read TextCh. 5 — Go in Production

HTTP and REST APIs in Go: The Standard Library Is Enough

One of Go's most surprising features is that its standard library includes a complete, production-grade HTTP server. Companies like Google, Cloudflare, and Dropbox have built systems handling millions of requests per second using net/http without external frameworks.

Go 1.22 significantly improved the standard ServeMux with support for HTTP method matching in patterns and path parameters, eliminating the need for third-party routers for most use cases.

The Simplest HTTP Server

package main

import (
    "fmt"
    "net/http"
)

func main() {
    http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
        fmt.Fprintf(w, "Hello, Go HTTP!")
    })

    // ListenAndServe blocks until the server stops
    http.ListenAndServe(":8080", nil)  // uses the global DefaultServeMux
}

`http.Request`: Accessing Request Data

func handler(w http.ResponseWriter, r *http.Request) {
    // HTTP method
    fmt.Println("Method:", r.Method)  // GET, POST, PUT, DELETE, etc.

    // URL and path
    fmt.Println("URL:", r.URL)
    fmt.Println("Path:", r.URL.Path)
    fmt.Println("Query:", r.URL.RawQuery)

    // Query parameters
    q := r.URL.Query()
    name := q.Get("name")   // ?name=Ana
    page := q.Get("page")   // ?page=2
    _ = name
    _ = page

    // Headers
    token := r.Header.Get("Authorization")
    contentType := r.Header.Get("Content-Type")
    _ = token
    _ = contentType

    // Body (for POST/PUT)
    defer r.Body.Close()
    data, err := io.ReadAll(r.Body)
    _ = err
    _ = data
}

`http.ResponseWriter`: Writing Responses

func handler(w http.ResponseWriter, r *http.Request) {
    // Headers must be set BEFORE the status and body
    w.Header().Set("Content-Type", "application/json")
    w.Header().Set("X-Custom-Header", "value")

    // Status code (can only be written once)
    w.WriteHeader(http.StatusCreated)  // 201

    // Body
    w.Write([]byte(`{"message": "created"}`))

    // Convenience helpers
    http.Error(w, "Not found", http.StatusNotFound)
    http.Redirect(w, r, "/new-path", http.StatusMovedPermanently)
}

JSON: Encoding and Decoding

import "encoding/json"

type User struct {
    ID    int    `json:"id"`
    Name  string `json:"name"`
    Email string `json:"email,omitempty"`
}

// Encode struct → JSON
func writeJSON(w http.ResponseWriter, status int, v any) error {
    w.Header().Set("Content-Type", "application/json; charset=utf-8")
    w.WriteHeader(status)
    return json.NewEncoder(w).Encode(v)
}

// Decode JSON → struct
func readJSON(r *http.Request, v any) error {
    defer r.Body.Close()
    // Limit body size to 1MB
    r.Body = http.MaxBytesReader(nil, r.Body, 1<<20)
    decoder := json.NewDecoder(r.Body)
    decoder.DisallowUnknownFields()  // reject unknown fields
    return decoder.Decode(v)
}

Go 1.22+ ServeMux: Advanced Pattern Matching

mux := http.NewServeMux()

// Pattern with HTTP method (new in Go 1.22)
mux.HandleFunc("GET /users", listUsers)
mux.HandleFunc("POST /users", createUser)
mux.HandleFunc("GET /users/{id}", getUser)
mux.HandleFunc("PUT /users/{id}", updateUser)
mux.HandleFunc("DELETE /users/{id}", deleteUser)

// Access path parameters
func getUser(w http.ResponseWriter, r *http.Request) {
    id := r.PathValue("id")  // r.PathValue available since Go 1.22
    // ...
}

// Subtree routing (trailing / captures any sub-route)
mux.HandleFunc("/static/", serveStaticFiles)

// Host-specific routing
mux.HandleFunc("api.my-app.com/v1/", apiV1Handler)

Middleware: Functions That Wrap Handlers

The middleware pattern in Go is simple: a function that receives an http.Handler and returns another http.Handler:

// Middleware type
type Middleware func(http.Handler) http.Handler

// Authentication middleware
func authenticate(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        token := r.Header.Get("Authorization")
        if token == "" {
            http.Error(w, "Unauthorized", http.StatusUnauthorized)
            return
        }
        // Verify token...
        next.ServeHTTP(w, r)
    })
}

// CORS middleware
func cors(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        w.Header().Set("Access-Control-Allow-Origin", "*")
        w.Header().Set("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
        w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")

        if r.Method == http.MethodOptions {
            w.WriteHeader(http.StatusNoContent)
            return
        }

        next.ServeHTTP(w, r)
    })
}

// Logging middleware with slog (Go 1.21+)
func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        rw := &responseWriter{ResponseWriter: w, status: http.StatusOK}
        next.ServeHTTP(rw, r)
        slog.Info("http",
            "method", r.Method,
            "path", r.URL.Path,
            "status", rw.status,
            "duration", time.Since(start),
        )
    })
}

type responseWriter struct {
    http.ResponseWriter
    status int
}

func (rw *responseWriter) WriteHeader(status int) {
    rw.status = status
    rw.ResponseWriter.WriteHeader(status)
}

// Chain middleware
func chain(h http.Handler, middlewares ...Middleware) http.Handler {
    for i := len(middlewares) - 1; i >= 0; i-- {
        h = middlewares[i](h)
    }
    return h
}

Complete REST API: Real Project Structure

// handlers/user.go
type UserHandler struct {
    repo *UserRepository
    log  *slog.Logger
}

func (h *UserHandler) List(w http.ResponseWriter, r *http.Request) {
    users, err := h.repo.All(r.Context())
    if err != nil {
        h.log.Error("error listing users", "error", err)
        writeError(w, http.StatusInternalServerError, "internal error")
        return
    }
    writeJSON(w, http.StatusOK, users)
}

func (h *UserHandler) Create(w http.ResponseWriter, r *http.Request) {
    var req struct {
        Name  string `json:"name"`
        Email string `json:"email"`
    }

    if err := readJSON(r, &req); err != nil {
        writeError(w, http.StatusBadRequest, "invalid JSON: "+err.Error())
        return
    }

    if req.Name == "" || req.Email == "" {
        writeError(w, http.StatusUnprocessableEntity, "name and email are required")
        return
    }

    user, err := h.repo.Create(r.Context(), req.Name, req.Email)
    if err != nil {
        writeError(w, http.StatusInternalServerError, "error creating user")
        return
    }

    writeJSON(w, http.StatusCreated, user)
}

// main.go
func main() {
    logger := slog.New(slog.NewJSONHandler(os.Stdout, nil))
    repo := NewUserRepository()
    handler := &UserHandler{repo: repo, log: logger}

    mux := http.NewServeMux()
    mux.HandleFunc("GET /api/v1/users", handler.List)
    mux.HandleFunc("POST /api/v1/users", handler.Create)
    mux.HandleFunc("GET /api/v1/users/{id}", handler.GetByID)

    srv := &http.Server{
        Addr:         ":8080",
        Handler:      chain(mux, logging, cors, recoverPanic),
        ReadTimeout:  10 * time.Second,
        WriteTimeout: 30 * time.Second,
        IdleTimeout:  120 * time.Second,
    }

    logger.Info("Server started", "addr", srv.Addr)
    if err := srv.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
        logger.Error("server error", "error", err)
        os.Exit(1)
    }
}

Serving Static Files

// Serve files from a directory
mux.Handle("GET /static/", http.StripPrefix("/static/",
    http.FileServer(http.Dir("./public"))))

// Using embed for files bundled into the binary
import "embed"

//go:embed public
var staticFiles embed.FS

mux.Handle("GET /static/", http.FileServerFS(staticFiles))

With net/http mastered, you are ready for the final project: you will build a CLI task manager with an HTTP API that applies everything you have learned in this course.

Go 1.22 dramatically improved ServeMux
Before Go 1.22, the standard ServeMux did not support HTTP method matching or path parameters. Go 1.22 introduced patterns like GET /users/{id} with r.PathValue("id"). For most REST APIs, you no longer need external routers like Gorilla Mux or Chi.
Always configure timeouts on http.Server
An http.Server without timeouts is vulnerable to slowloris attacks and can exhaust connections in production. Always configure ReadTimeout, WriteTimeout, and ideally IdleTimeout. For APIs, 10-30 second values are reasonable. For large uploads, use ReadHeaderTimeout separately.