Skip to main content

Go Ep 15: Building a Production REST API

Rachmat Hidayat
Author
Rachmat Hidayat
Learn & sharing insights on TypeScript, Go, Kubernetes, DevOps, DevSecOps, SRE, Platform Engineering, AI/ML Engineering, and MLOps.
go - This article is part of a series.
Part 15: This Article
Theory is useless without execution. We assemble everything learned across 14 episodes — structs, pointers, error handling, JSON codecs, context timeouts, and concurrency — into a production-grade HTTP REST API.

TL;DR (Quick Summary)
#

  • Go 1.22+ Routing: Native HTTP method routing and path wildcards (http.NewServeMux().HandleFunc("POST /api/v1/users", ...)).
  • Streaming Codecs: json.NewDecoder(r.Body).Decode(&req) processes payloads directly from memory streams.
  • Middleware Pipeline: Reusable handler wrappers for Logging, CORS, Authentication, and Rate Limiting.
  • Graceful Shutdown: Intercepts SIGINT/SIGTERM signals and uses server.Shutdown(ctx) to complete active requests before closing.

1. Production API Architecture
#


graph LR
    Client["HTTP Client / Frontend"] -->|POST /users| Logging["Logging Middleware"]
    Logging -->|Passes Context| Auth["Auth Middleware"]
    Auth -->|Valid Request| Router["ServeMux Router"]
    Router -->|Invokes| Handler["CreateUser Handler"]
    Handler -->|Returns JSON 201| Client

2. Step-by-Step Lab: Building the Production API
#

Create main.go:

package main

import (
 "context"
 "encoding/json"
 "errors"
 "fmt"
 "log"
 "net/http"
 "os"
 "os/signal"
 "syscall"
 "time"
)

// User Model
type User struct {
 ID        string    `json:"id"`
 Username  string    `json:"username"`
 Email     string    `json:"email"`
 CreatedAt time.Time `json:"created_at"`
}

type CreateUserRequest struct {
 Username string `json:"username"`
 Email    string `json:"email"`
}

// In-Memory Database
var users = map[string]User{
 "1": {ID: "1", Username: "rhidayat", Email: "[email protected]", CreatedAt: time.Now()},
}

// Middleware: Request Logging
func LoggingMiddleware(next http.Handler) http.Handler {
 return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  start := time.Now()
  log.Printf("--> %s %s", r.Method, r.URL.Path)
  next.ServeHTTP(w, r)
  log.Printf("<-- %s %s completed in %v", r.Method, r.URL.Path, time.Since(start))
 })
}

// Handler: GET /users/{id}
func GetUserHandler(w http.ResponseWriter, r *http.Request) {
 id := r.PathValue("id")

 user, exists := users[id]
 if !exists {
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(http.StatusNotFound)
  json.NewEncoder(w).Encode(map[string]string{"error": "User not found"})
  return
 }

 w.Header().Set("Content-Type", "application/json")
 w.WriteHeader(http.StatusOK)
 json.NewEncoder(w).Encode(user)
}

// Handler: POST /users
func CreateUserHandler(w http.ResponseWriter, r *http.Request) {
 var req CreateUserRequest
 if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
  w.Header().Set("Content-Type", "application/json")
  w.WriteHeader(http.StatusBadRequest)
  json.NewEncoder(w).Encode(map[string]string{"error": "Invalid JSON payload"})
  return
 }

 newID := fmt.Sprintf("%d", len(users)+1)
 newUser := User{
  ID:        newID,
  Username:  req.Username,
  Email:     req.Email,
  CreatedAt: time.Now(),
 }
 users[newID] = newUser

 w.Header().Set("Content-Type", "application/json")
 w.WriteHeader(http.StatusCreated)
 json.NewEncoder(w).Encode(newUser)
}

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

 // Register Routes
 mux.HandleFunc("GET /users/{id}", GetUserHandler)
 mux.HandleFunc("POST /users", CreateUserHandler)

 handler := LoggingMiddleware(mux)

 server := &http.Server{
  Addr:         ":8080",
  Handler:      handler,
  ReadTimeout:  5 * time.Second,
  WriteTimeout: 10 * time.Second,
 }

 // Start server in background
 go func() {
  log.Println("🚀 Go REST API running on http://localhost:8080")
  if err := server.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
   log.Fatalf("Server crash: %v", err)
  }
 }()

 // Graceful Shutdown
 stop := make(chan os.Signal, 1)
 signal.Notify(stop, os.Interrupt, syscall.SIGTERM)

 <-stop
 log.Println("⚠️  Shutting down API server...")

 ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
 defer cancel()

 if err := server.Shutdown(ctx); err != nil {
  log.Fatalf("Graceful shutdown failed: %v", err)
 }

 log.Println("✅ Server stopped gracefully.")
}

3. Testing API Endpoints
#

Start the server:

go run main.go

Test requests via curl:

# GET User
curl -s http://localhost:8080/users/1

# POST User
curl -s -X POST http://localhost:8080/users \
  -H "Content-Type: application/json" \
  -d '{"username":"alice","email":"[email protected]"}'

Summary & Conclusion
#

Congratulations! You have completed the 15-Episode Go Master Series!

You have progressed from:

  • Installation, syntax, and zero values
  • Structs, maps, slices, and pointers
  • Memory stack vs heap escape analysis
  • Implicit interfaces, type assertions, and custom domain errors
  • Goroutines, channels, G-M-P scheduler, and worker pools
  • Unit testing, benchmarking, fuzzing, and zero-dependency REST API servers

Keep building high-performance Go applications!

go - This article is part of a series.
Part 15: This Article