REST API Server with Middleware
Implements a small user API with middleware, structured routing, and guarded state access.
main.go
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"strconv"
"sync"
"time"
"github.com/gorilla/mux"
)
// User represents a user resource
type User struct {
ID int `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Age int `json:"age"`
}
// UserStore manages user data with thread-safe operations
type UserStore struct {
mu sync.RWMutex
users map[int]User
nextID int
}
// NewUserStore creates a new UserStore with initial data
func NewUserStore() *UserStore {
return &UserStore{
users: map[int]User{
1: {ID: 1, Username: "john_doe", Email: "[email protected]", Age: 30},
2: {ID: 2, Username: "jane_smith", Email: "[email protected]", Age: 28},
},
nextID: 3,
}
}
// GetAllUsers returns all users (read-locked for thread safety)
func (us *UserStore) GetAllUsers() []User {
us.mu.RLock()
defer us.mu.RUnlock()
users := make([]User, 0, len(us.users))
for _, user := range us.users {
users = append(users, user)
}
return users
}
// GetUserByID returns a single user by ID
func (us *UserStore) GetUserByID(id int) (User, bool) {
us.mu.RLock()
defer us.mu.RUnlock()
user, exists := us.users[id]
return user, exists
}
// CreateUser adds a new user to the store
func (us *UserStore) CreateUser(user User) User {
us.mu.Lock()
defer us.mu.Unlock()
user.ID = us.nextID
us.users[user.ID] = user
us.nextID++
return user
}
// UpdateUser updates an existing user
func (us *UserStore) UpdateUser(id int, updatedUser User) (User, bool) {
us.mu.Lock()
defer us.mu.Unlock()
user, exists := us.users[id]
if !exists {
return User{}, false
}
// Preserve original ID and update other fields
updatedUser.ID = id
us.users[id] = updatedUser
return updatedUser, true
}
// DeleteUser removes a user from the store
func (us *UserStore) DeleteUser(id int) bool {
us.mu.Lock()
defer us.mu.Unlock()
_, exists := us.users[id]
if !exists {
return false
}
delete(us.users, id)
return true
}
// Middleware: LoggingMiddleware logs incoming HTTP requests
func LoggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf(
"START: %s %s %s",
r.Method,
r.URL.Path,
r.RemoteAddr,
)
// Call the next handler
next.ServeHTTP(w, r)
// Log after request is processed
log.Printf(
"END: %s %s %s %s",
r.Method,
r.URL.Path,
r.RemoteAddr,
time.Since(start),
)
})
}
// Middleware: RecoveryMiddleware recovers from panics and returns 500 error
func RecoveryMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
defer func() {
if err := recover(); err != nil {
log.Printf("PANIC: %v", err)
http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError)
}
}()
next.ServeHTTP(w, r)
})
}
// Helper: respondJSON sends a JSON response with proper status code
func respondJSON(w http.ResponseWriter, status int, data interface{}) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
if data != nil {
if err := json.NewEncoder(w).Encode(data); err != nil {
log.Printf("Failed to encode JSON response: %v", err)
}
}
}
// Handler: GetAllUsersHandler returns all users
func GetAllUsersHandler(us *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
users := us.GetAllUsers()
respondJSON(w, http.StatusOK, users)
}
}
// Handler: GetUserHandler returns a single user by ID
func GetUserHandler(us *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
return
}
user, exists := us.GetUserByID(id)
if !exists {
respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
return
}
respondJSON(w, http.StatusOK, user)
}
}
// Handler: CreateUserHandler creates a new user
func CreateUserHandler(us *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
// Validate required fields
if user.Username == "" || user.Email == "" {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "username and email are required"})
return
}
createdUser := us.CreateUser(user)
respondJSON(w, http.StatusCreated, createdUser)
}
}
// Handler: UpdateUserHandler updates an existing user
func UpdateUserHandler(us *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
return
}
var updatedUser User
if err := json.NewDecoder(r.Body).Decode(&updatedUser); err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid request body"})
return
}
user, exists := us.UpdateUser(id, updatedUser)
if !exists {
respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
return
}
respondJSON(w, http.StatusOK, user)
}
}
// Handler: DeleteUserHandler deletes a user
func DeleteUserHandler(us *UserStore) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
vars := mux.Vars(r)
idStr := vars["id"]
id, err := strconv.Atoi(idStr)
if err != nil {
respondJSON(w, http.StatusBadRequest, map[string]string{"error": "invalid user ID"})
return
}
success := us.DeleteUser(id)
if !success {
respondJSON(w, http.StatusNotFound, map[string]string{"error": "user not found"})
return
}
// Return 204 No Content on successful deletion
w.WriteHeader(http.StatusNoContent)
}
}
func main() {
// Initialize user store
userStore := NewUserStore()
// Create router with gorilla/mux (industry standard)
r := mux.NewRouter()
// Apply global middleware
r.Use(LoggingMiddleware)
r.Use(RecoveryMiddleware)
// Define API routes
api := r.PathPrefix("/api/v1").Subrouter()
api.HandleFunc("/users", GetAllUsersHandler(userStore)).Methods(http.MethodGet)
api.HandleFunc("/users/{id}", GetUserHandler(userStore)).Methods(http.MethodGet)
api.HandleFunc("/users", CreateUserHandler(userStore)).Methods(http.MethodPost)
api.HandleFunc("/users/{id}", UpdateUserHandler(userStore)).Methods(http.MethodPut)
api.HandleFunc("/users/{id}", DeleteUserHandler(userStore)).Methods(http.MethodDelete)
// Get port from environment variable (default to 8080)
port := os.Getenv("PORT")
if port == "" {
port = "8080"
}
// Start server
log.Printf("Starting server on :%s", port)
if err := http.ListenAndServe(":"+port, r); err != nil {
log.Fatalf("Server failed to start: %v", err)
}
}How It Works
Implements a small user API with middleware, structured routing, and guarded state access.
UserStore holds users behind an RWMutex with auto-incremented IDs; handlers decode and validate JSON bodies, then perform CRUD operations; router groups endpoints under /api/v1; middleware logs method, path, and duration and converts panics into 500 responses; server reads the PORT environment variable before ListenAndServe.
Key Concepts
- 1Thread-safe map storage with read/write locks and incremental ID tracking.
- 2JSON decoding and encoding with validation of required fields and path parameters.
- 3Reusable logging and recovery middleware around all routes.
- 4Versioned subrouter and explicit HTTP methods for each handler.
When to Use This Pattern
- Bootstrap template for CRUD microservices or prototypes.
- Mock API for frontend integration tests.
- Teaching Gorilla Mux routing and middleware layering.
- Temporary admin APIs before adding a database.
Best Practices
- Return clear 4xx errors for validation failures instead of panicking.
- Add context timeouts for database or downstream calls when extending handlers.
- Keep route registration together and versioned for future migrations.
- Log request duration and status codes for observability.
Go Version1.18+
Difficultyadvanced
Production ReadyYes
Lines of Code280