Back to Blog
Go2025-02-10

Building REST APIs with Go

Build a production-ready REST API with Go using net/http, routing, middleware, JSON handling, and best practices.

Setting Up

Go's standard library net/http is powerful enough for most REST APIs. With Go 1.22+, the built-in router supports method-based routing.

``go package main

import ( "encoding/json" "log" "net/http" )

func main() { mux := http.NewServeMux()

// Go 1.22+ pattern matching mux.HandleFunc("GET /api/users", getUsers) mux.HandleFunc("GET /api/users/{id}", getUser) mux.HandleFunc("POST /api/users", createUser) mux.HandleFunc("PUT /api/users/{id}", updateUser) mux.HandleFunc("DELETE /api/users/{id}", deleteUser)

log.Println("Server starting on :8080") log.Fatal(http.ListenAndServe(":8080", mux)) } `

Defining Models

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

// In-memory store var users = map[string]User{ "1": {ID: "1", Name: "Alice", Email: "alice@example.com", Age: 28}, "2": {ID: "2", Name: "Bob", Email: "bob@example.com", Age: 32}, } `

Handler Functions

`go func getUsers(w http.ResponseWriter, r *http.Request) { userList := make([]User, 0, len(users)) for _, u := range users { userList = append(userList, u) } writeJSON(w, http.StatusOK, userList) }

func getUser(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") user, ok := users[id] if !ok { writeJSON(w, http.StatusNotFound, map[string]string{ "error": "user not found", }) return } writeJSON(w, http.StatusOK, user) }

func createUser(w http.ResponseWriter, r *http.Request) { var user User if err := json.NewDecoder(r.Body).Decode(&user); err != nil { writeJSON(w, http.StatusBadRequest, map[string]string{ "error": "invalid JSON", }) return } users[user.ID] = user writeJSON(w, http.StatusCreated, user) } `

JSON Helper

`go func writeJSON(w http.ResponseWriter, status int, data any) { w.Header().Set("Content-Type", "application/json") w.WriteHeader(status) json.NewEncoder(w).Encode(data) } `

Middleware

`go func loggingMiddleware(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)) }) }

func corsMiddleware(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") w.Header().Set("Access-Control-Allow-Headers", "Content-Type") if r.Method == "OPTIONS" { w.WriteHeader(http.StatusOK) return } next.ServeHTTP(w, r) }) }

// Apply middleware handler := loggingMiddleware(corsMiddleware(mux)) http.ListenAndServe(":8080", handler) `

Structured Server

`go type Server struct { store map[string]User router *http.ServeMux }

func NewServer() *Server { s := &Server{ store: make(map[string]User), router: http.NewServeMux(), } s.routes() return s }

func (s *Server) routes() { s.router.HandleFunc("GET /api/users", s.handleGetUsers) s.router.HandleFunc("POST /api/users", s.handleCreateUser) }

func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) { s.router.ServeHTTP(w, r) } ``

Best Practices

1. Use structured logging (slog in Go 1.21+) 2. Always validate input before processing 3. Use context for request-scoped values and cancellation 4. Graceful shutdown with signal handling 5. Keep handlers thin — business logic in separate packages 6. Use middleware for cross-cutting concerns (auth, logging, CORS)