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)