Build REST Apis
Problem
Building RESTful APIs requires handling HTTP requests, routing, JSON serialization, validation, and error handling consistently. The standard library is low-level, requiring significant boilerplate.
// Problematic approach - no structure
http.HandleFunc("/users", func(w http.ResponseWriter, r *http.Request) {
// All HTTP methods in one handler
// No validation, poor error handling
fmt.Fprintf(w, "Users endpoint")
})This guide shows practical techniques for building production-ready REST APIs in Go.
Solution
1. Basic REST API Structure
Simple REST API with net/http:
package main
import (
"encoding/json"
"log"
"net/http"
"strconv"
"sync"
)
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
}
type UserStore struct {
mu sync.RWMutex
users map[int]User
nextID int
}
func NewUserStore() *UserStore {
return &UserStore{
users: make(map[int]User),
nextID: 1,
}
}
func (s *UserStore) Create(user User) User {
s.mu.Lock()
defer s.mu.Unlock()
user.ID = s.nextID
s.users[user.ID] = user
s.nextID++
return user
}
func (s *UserStore) GetAll() []User {
s.mu.RLock()
defer s.mu.RUnlock()
users := make([]User, 0, len(s.users))
for _, user := range s.users {
users = append(users, user)
}
return users
}
func (s *UserStore) Get(id int) (User, bool) {
s.mu.RLock()
defer s.mu.RUnlock()
user, exists := s.users[id]
return user, exists
}
type Server struct {
store *UserStore
}
func (s *Server) handleUsers(w http.ResponseWriter, r *http.Request) {
switch r.Method {
case http.MethodGet:
s.getUsers(w, r)
case http.MethodPost:
s.createUser(w, r)
default:
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) getUsers(w http.ResponseWriter, r *http.Request) {
users := s.store.GetAll()
respondJSON(w, http.StatusOK, users)
}
func (s *Server) createUser(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
respondError(w, http.StatusBadRequest, "invalid request body")
return
}
if user.Username == "" || user.Email == "" {
respondError(w, http.StatusBadRequest, "username and email required")
return
}
created := s.store.Create(user)
respondJSON(w, http.StatusCreated, created)
}
func (s *Server) handleUser(w http.ResponseWriter, r *http.Request) {
// Extract ID from path (simple parsing)
idStr := r.URL.Path[len("/users/"):]
id, err := strconv.Atoi(idStr)
if err != nil {
respondError(w, http.StatusBadRequest, "invalid user id")
return
}
switch r.Method {
case http.MethodGet:
s.getUser(w, r, id)
case http.MethodDelete:
s.deleteUser(w, r, id)
default:
respondError(w, http.StatusMethodNotAllowed, "method not allowed")
}
}
func (s *Server) getUser(w http.ResponseWriter, r *http.Request, id int) {
user, exists := s.store.Get(id)
if !exists {
respondError(w, http.StatusNotFound, "user not found")
return
}
respondJSON(w, http.StatusOK, user)
}
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(data)
}
func respondError(w http.ResponseWriter, status int, message string) {
respondJSON(w, status, map[string]string{"error": message})
}
func main() {
server := &Server{store: NewUserStore()}
http.HandleFunc("/users", server.handleUsers)
http.HandleFunc("/users/", server.handleUser)
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", nil))
}2. Router with gorilla/mux
Advanced routing:
import "github.com/gorilla/mux"
func setupRouter() *mux.Router {
r := mux.NewRouter()
// Route with path variables
r.HandleFunc("/users/{id:[0-9]+}", getUser).Methods("GET")
r.HandleFunc("/users/{id:[0-9]+}", updateUser).Methods("PUT")
r.HandleFunc("/users/{id:[0-9]+}", deleteUser).Methods("DELETE")
r.HandleFunc("/users", createUser).Methods("POST")
r.HandleFunc("/users", listUsers).Methods("GET")
// Query parameters
r.HandleFunc("/search", searchUsers).Methods("GET")
// Subrouters
api := r.PathPrefix("/api/v1").Subrouter()
api.HandleFunc("/users", listUsers).Methods("GET")
return r
}
func getUser(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
id := vars["id"]
// Use id
respondJSON(w, http.StatusOK, map[string]string{"id": id})
}
func searchUsers(w http.ResponseWriter, r *http.Request) {
query := r.URL.Query()
email := query.Get("email")
name := query.Get("name")
// Search with parameters
respondJSON(w, http.StatusOK, map[string]string{
"email": email,
"name": name,
})
}3. Middleware Pattern
HTTP middleware:
type Middleware func(http.Handler) http.Handler
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
log.Printf("%s %s %s", r.RemoteAddr, r.Method, r.URL.Path)
next.ServeHTTP(w, r)
})
}
func authMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
token := r.Header.Get("Authorization")
if token != "Bearer secret-token" {
respondError(w, http.StatusUnauthorized, "unauthorized")
return
}
next.ServeHTTP(w, r)
})
}
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, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
func chainMiddleware(h http.Handler, middlewares ...Middleware) http.Handler {
for i := len(middlewares) - 1; i >= 0; i-- {
h = middlewares[i](h)
}
return h
}
func main() {
r := setupRouter()
handler := chainMiddleware(r,
loggingMiddleware,
corsMiddleware,
authMiddleware,
)
log.Fatal(http.ListenAndServe(":8080", handler))
}4. Request Validation
Input validation:
import "github.com/go-playground/validator/v10"
type CreateUserRequest struct {
Username string `json:"username" validate:"required,min=3,max=50"`
Email string `json:"email" validate:"required,email"`
Age int `json:"age" validate:"gte=18,lte=120"`
}
var validate = validator.New()
func createUserHandler(w http.ResponseWriter, r *http.Request) {
var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON")
return
}
if err := validate.Struct(req); err != nil {
errors := make(map[string]string)
for _, err := range err.(validator.ValidationErrors) {
errors[err.Field()] = err.Tag()
}
respondJSON(w, http.StatusBadRequest, map[string]interface{}{
"error": "validation failed",
"fields": errors,
})
return
}
// Create user
respondJSON(w, http.StatusCreated, req)
}How It Works
HTTP Request Flow
%% Color Palette: Blue #0173B2, Orange #DE8F05, Teal #029E73, Purple #CC78BC
graph TD
A[HTTP Request] --> B[Middleware Chain]
B --> C[Logging]
C --> D[CORS]
D --> E[Authentication]
E --> F[Router]
F --> G[Handler]
G --> H{Validation}
H -->|Pass| I[Business Logic]
H -->|Fail| J[Error Response]
I --> K[JSON Response]
J --> L[HTTP Response]
K --> L
style A fill:#0173B2,stroke:#000000,color:#FFFFFF
style B fill:#DE8F05,stroke:#000000,color:#FFFFFF
style F fill:#029E73,stroke:#000000,color:#FFFFFF
style I fill:#CC78BC,stroke:#000000,color:#FFFFFF
Variations
Chi Router
import "github.com/go-chi/chi/v5"
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{userID}", func(r chi.Router) {
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})Gin Framework
import "github.com/gin-gonic/gin"
r := gin.Default()
r.GET("/users/:id", func(c *gin.Context) {
id := c.Param("id")
c.JSON(200, gin.H{"id": id})
})
r.POST("/users", func(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(400, gin.H{"error": err.Error()})
return
}
c.JSON(201, user)
})Common Pitfalls
Pitfall 1: Not Handling Errors
// Bad
json.NewDecoder(r.Body).Decode(&user)
// Good
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
respondError(w, http.StatusBadRequest, "invalid JSON")
return
}Pitfall 2: Missing Content-Type
// Bad
json.NewEncoder(w).Encode(data)
// Good
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(data)Pitfall 3: Not Closing Request Body
// Good practice
defer r.Body.Close()Related Patterns
Related Tutorial: See Intermediate Tutorial - Web Development and Beginner Tutorial - HTTP Basics.
Related How-To: See Implement Middleware and Work with JSON.
Related Cookbook: See Cookbook recipes “REST API Patterns”, “Request Validation”, “Error Handling”.
Further Reading
Last updated