On this page
HTTP and REST APIs
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.
Sign in to track your progress