Go HTTP Frameworks & REST APIs Overview
Go provides exceptional HTTP capabilities starting with the standard library's net/http package. Go 1.22+ introduced enhanced pattern routing in ServeMux, making stdlib viable for many applications. For more complex needs, frameworks like Chi, Gin, Echo, and Fiber offer additional features while maintaining Go's simplicity and performance.
Key Features:
🌐 net/http: Production-ready standard library with Go 1.22+ routing 🎯 Chi: Lightweight, stdlib-compatible router with middleware chains ⚡ Gin: High-performance framework with binding and validation 🛡️ Echo: Type-safe, enterprise framework with OpenAPI support 🚀 Fiber: Express.js-inspired framework with WebSocket support 🔧 Middleware: Composable request/response processing ✅ Validation: Struct tag-based request validation 🧪 Testing: httptest.Server for comprehensive integration tests When to Use This Skill
Activate this skill when:
Building RESTful APIs or web services Choosing appropriate HTTP framework for project requirements Implementing authentication or authorization middleware Designing REST endpoint patterns and validation Testing HTTP handlers and middleware chains Optimizing API performance and response times Migrating between HTTP frameworks Framework Selection Guide net/http (Standard Library) - Go 1.22+
Use When:
Building simple to moderate complexity APIs Avoiding external dependencies is priority Need maximum compatibility and long-term stability Team prefers explicit over implicit patterns
Strengths:
Zero dependencies, part of Go standard library Go 1.22+ pattern routing with path parameters Excellent performance and stability Extensive ecosystem compatibility No framework lock-in
Limitations:
More verbose middleware composition Manual request validation No built-in binding or rendering
Example:
package main
import ( "encoding/json" "net/http" "log" )
// Go 1.22+ pattern routing func main() { mux := http.NewServeMux()
// Path parameters with {param} syntax
mux.HandleFunc("GET /users/{id}", getUserHandler)
mux.HandleFunc("POST /users", createUserHandler)
mux.HandleFunc("GET /users", listUsersHandler)
// Middleware wrapping
handler := loggingMiddleware(mux)
log.Fatal(http.ListenAndServe(":8080", handler))
}
func getUserHandler(w http.ResponseWriter, r *http.Request) { id := r.PathValue("id") // Go 1.22+ path parameter extraction
user := User{ID: id, Name: "John Doe"}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(user)
}
func loggingMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { log.Printf("%s %s", r.Method, r.URL.Path) next.ServeHTTP(w, r) }) }
type User struct {
ID string json:"id"
Name string json:"name"
}
Chi - Lightweight Router
Use When:
Want stdlib-compatible router with better ergonomics Need clean middleware composition Prefer explicit over magic patterns Building moderate to complex routing structures
Strengths:
100% compatible with net/http Excellent middleware ecosystem Route grouping and nesting Context-based parameter passing Minimal performance overhead
Installation:
go get -u github.com/go-chi/chi/v5
Example:
package main
import ( "encoding/json" "net/http" "github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5/middleware" )
func main() { r := chi.NewRouter()
// Built-in middleware
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Use(middleware.RequestID)
// Route grouping
r.Route("/api/v1", func(r chi.Router) {
r.Route("/users", func(r chi.Router) {
r.Get("/", listUsers)
r.Post("/", createUser)
r.Route("/{userID}", func(r chi.Router) {
r.Use(UserContext) // Middleware for nested routes
r.Get("/", getUser)
r.Put("/", updateUser)
r.Delete("/", deleteUser)
})
})
})
http.ListenAndServe(":8080", r)
}
func UserContext(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { userID := chi.URLParam(r, "userID") // Load user from database, set in context ctx := context.WithValue(r.Context(), "user", userID) next.ServeHTTP(w, r.WithContext(ctx)) }) }
func getUser(w http.ResponseWriter, r *http.Request) { userID := r.Context().Value("user").(string) // Return user data json.NewEncoder(w).Encode(map[string]string{"id": userID}) }
Gin - High Performance Framework
Use When:
Need maximum performance (8x faster than most frameworks) Want batteries-included experience Require built-in validation and binding Building JSON APIs with minimal boilerplate
Strengths:
Extremely fast (fastest Go framework in benchmarks) Built-in JSON binding and validation Middleware ecosystem Group-based routing Custom error handling
Installation:
go get -u github.com/gin-gonic/gin
Example:
package main
import ( "net/http" "github.com/gin-gonic/gin" )
type CreateUserRequest struct {
Name string json:"name" binding:"required,min=3"
Email string json:"email" binding:"required,email"
Age int json:"age" binding:"required,gte=18"
}
func main() { r := gin.Default() // Logger + Recovery middleware
api := r.Group("/api/v1")
{
users := api.Group("/users")
{
users.GET("", listUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
}
r.Run(":8080")
}
func createUser(c *gin.Context) { var req CreateUserRequest
// Automatic validation based on struct tags
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// Process user creation
user := User{
ID: generateID(),
Name: req.Name,
Email: req.Email,
Age: req.Age,
}
c.JSON(http.StatusCreated, user)
}
func getUser(c *gin.Context) { id := c.Param("id")
// Return user
c.JSON(http.StatusOK, gin.H{
"id": id,
"name": "John Doe",
})
}
Echo - Enterprise Framework
Use When:
Building enterprise applications Need OpenAPI/Swagger integration Want comprehensive middleware library Require type-safe routing and binding
Strengths:
Type-safe routing with automatic parameter binding Built-in middleware for common patterns OpenAPI/Swagger generation support Excellent error handling middleware WebSocket support
Installation:
go get -u github.com/labstack/echo/v4
Example:
package main
import ( "net/http" "github.com/labstack/echo/v4" "github.com/labstack/echo/v4/middleware" )
func main() { e := echo.New()
// Middleware
e.Use(middleware.Logger())
e.Use(middleware.Recover())
e.Use(middleware.CORS())
// Routes
e.GET("/users/:id", getUser)
e.POST("/users", createUser)
e.PUT("/users/:id", updateUser)
e.DELETE("/users/:id", deleteUser)
e.Logger.Fatal(e.Start(":8080"))
}
func getUser(c echo.Context) error { id := c.Param("id")
user := User{ID: id, Name: "John Doe"}
return c.JSON(http.StatusOK, user)
}
func createUser(c echo.Context) error { var user User
if err := c.Bind(&user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
if err := c.Validate(&user); err != nil {
return echo.NewHTTPError(http.StatusBadRequest, err.Error())
}
return c.JSON(http.StatusCreated, user)
}
// Custom error handler func customErrorHandler(err error, c echo.Context) { code := http.StatusInternalServerError message := "Internal Server Error"
if he, ok := err.(*echo.HTTPError); ok {
code = he.Code
message = he.Message.(string)
}
c.JSON(code, map[string]string{"error": message})
}
Fiber - Express.js Style
Use When:
Team familiar with Express.js patterns Need WebSocket support out of the box Building real-time applications Want fastest route matching performance
Strengths:
Express.js-inspired API (easy for Node.js developers) Fastest route matching (uses fasthttp) Built-in WebSocket support Template engine support File upload handling
Limitations:
Uses fasthttp instead of net/http (less ecosystem compatibility) Not compatible with standard http.Handler interface Slightly less mature ecosystem
Installation:
go get -u github.com/gofiber/fiber/v2
Example:
package main
import ( "github.com/gofiber/fiber/v2" "github.com/gofiber/fiber/v2/middleware/logger" "github.com/gofiber/fiber/v2/middleware/cors" )
func main() { app := fiber.New()
// Middleware
app.Use(logger.New())
app.Use(cors.New())
// Routes
api := app.Group("/api/v1")
users := api.Group("/users")
users.Get("/", listUsers)
users.Post("/", createUser)
users.Get("/:id", getUser)
users.Put("/:id", updateUser)
users.Delete("/:id", deleteUser)
app.Listen(":8080")
}
func getUser(c *fiber.Ctx) error { id := c.Params("id")
return c.JSON(fiber.Map{
"id": id,
"name": "John Doe",
})
}
func createUser(c *fiber.Ctx) error { var user User
if err := c.BodyParser(&user); err != nil {
return c.Status(400).JSON(fiber.Map{"error": err.Error()})
}
return c.Status(201).JSON(user)
}
Common HTTP Patterns Request Validation
Struct Tag Validation:
import "github.com/go-playground/validator/v10"
type CreateUserRequest struct {
Name string json:"name" validate:"required,min=3,max=50"
Email string json:"email" validate:"required,email"
Age int json:"age" validate:"required,gte=18,lte=120"
Password string json:"password" validate:"required,min=8"
}
var validate = validator.New()
func validateRequest(req interface{}) error { return validate.Struct(req) }
// Usage func createUser(w http.ResponseWriter, r *http.Request) { var req CreateUserRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
if err := validateRequest(&req); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
// Process valid request
}
Custom Validators:
import "github.com/go-playground/validator/v10"
var validate = validator.New()
func init() { validate.RegisterValidation("username", validateUsername) }
func validateUsername(fl validator.FieldLevel) bool { username := fl.Field().String() // Custom validation logic return len(username) >= 3 && isAlphanumeric(username) }
type SignupRequest struct {
Username string validate:"required,username"
Email string validate:"required,email"
}
Middleware Patterns
Authentication Middleware:
func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { token := r.Header.Get("Authorization")
if token == "" {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
// Validate token
userID, err := validateToken(token)
if err != nil {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
// Add user to context
ctx := context.WithValue(r.Context(), "userID", userID)
next.ServeHTTP(w, r.WithContext(ctx))
})
}
Rate Limiting Middleware:
import ( "golang.org/x/time/rate" "sync" )
type RateLimiter struct { limiters map[string]*rate.Limiter mu sync.RWMutex rate rate.Limit burst int }
func NewRateLimiter(r rate.Limit, b int) RateLimiter { return &RateLimiter{ limiters: make(map[string]rate.Limiter), rate: r, burst: b, } }
func (rl RateLimiter) getLimiter(ip string) rate.Limiter { rl.mu.Lock() defer rl.mu.Unlock()
limiter, exists := rl.limiters[ip]
if !exists {
limiter = rate.NewLimiter(rl.rate, rl.burst)
rl.limiters[ip] = limiter
}
return limiter
}
func (rl RateLimiter) Middleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r http.Request) { ip := r.RemoteAddr limiter := rl.getLimiter(ip)
if !limiter.Allow() {
http.Error(w, "Rate limit exceeded", http.StatusTooManyRequests)
return
}
next.ServeHTTP(w, r)
})
}
CORS Middleware:
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, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization")
if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}
next.ServeHTTP(w, r)
})
}
Error Handling Strategies
Custom Error Types:
type APIError struct {
Code int json:"code"
Message string json:"message"
Details string json:"details,omitempty"
}
func (e *APIError) Error() string { return e.Message }
// Error constructors func NewBadRequestError(msg string) *APIError { return &APIError{Code: http.StatusBadRequest, Message: msg} }
func NewNotFoundError(msg string) *APIError { return &APIError{Code: http.StatusNotFound, Message: msg} }
func NewInternalError(msg string) *APIError { return &APIError{Code: http.StatusInternalServerError, Message: msg} }
Error Response Middleware:
type APIHandler func(w http.ResponseWriter, r *http.Request) error
func ErrorHandlerMiddleware(h APIHandler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { err := h(w, r) if err == nil { return }
// Handle different error types
var apiErr *APIError
if errors.As(err, &apiErr) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(apiErr.Code)
json.NewEncoder(w).Encode(apiErr)
return
}
// Unknown error - log and return generic message
log.Printf("Internal error: %v", err)
w.WriteHeader(http.StatusInternalServerError)
json.NewEncoder(w).Encode(APIError{
Code: http.StatusInternalServerError,
Message: "Internal server error",
})
})
}
// Usage func getUserHandler(w http.ResponseWriter, r *http.Request) error { id := r.PathValue("id")
user, err := db.GetUser(id)
if err != nil {
if errors.Is(err, sql.ErrNoRows) {
return NewNotFoundError("User not found")
}
return fmt.Errorf("database error: %w", err)
}
w.Header().Set("Content-Type", "application/json")
return json.NewEncoder(w).Encode(user)
}
// Register with middleware mux.Handle("GET /users/{id}", ErrorHandlerMiddleware(getUserHandler))
REST API Design Patterns
Resource Naming Conventions:
// Good: Plural nouns for collections GET /api/v1/users // List users POST /api/v1/users // Create user GET /api/v1/users/{id} // Get user PUT /api/v1/users/{id} // Update user (full) PATCH /api/v1/users/{id} // Update user (partial) DELETE /api/v1/users/{id} // Delete user
// Nested resources GET /api/v1/users/{id}/posts // User's posts POST /api/v1/users/{id}/posts // Create post for user GET /api/v1/users/{id}/posts/{pid} // Specific post
// Avoid: Verbs in URLs (use HTTP methods instead) // Bad: POST /api/v1/users/create // Bad: GET /api/v1/users/get/{id}
Pagination Pattern:
type PaginationParams struct {
Page int json:"page" validate:"gte=1"
PageSize int json:"page_size" validate:"gte=1,lte=100"
}
type PaginatedResponse struct {
Data interface{} json:"data"
Page int json:"page"
PageSize int json:"page_size"
TotalCount int json:"total_count"
TotalPages int json:"total_pages"
}
func listUsers(w http.ResponseWriter, r *http.Request) { // Parse pagination params page, _ := strconv.Atoi(r.URL.Query().Get("page")) if page < 1 { page = 1 }
pageSize, _ := strconv.Atoi(r.URL.Query().Get("page_size"))
if pageSize < 1 || pageSize > 100 {
pageSize = 20
}
// Fetch data
users, totalCount := db.GetUsers(page, pageSize)
totalPages := (totalCount + pageSize - 1) / pageSize
response := PaginatedResponse{
Data: users,
Page: page,
PageSize: pageSize,
TotalCount: totalCount,
TotalPages: totalPages,
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}
Query Parameter Filtering:
type UserFilter struct {
Status string json:"status"
Role string json:"role"
CreatedAt string json:"created_at"
Search string json:"search"
}
func parseFilters(r *http.Request) UserFilter { return UserFilter{ Status: r.URL.Query().Get("status"), Role: r.URL.Query().Get("role"), CreatedAt: r.URL.Query().Get("created_at"), Search: r.URL.Query().Get("search"), } }
func listUsers(w http.ResponseWriter, r *http.Request) { filters := parseFilters(r)
users := db.GetUsersWithFilters(filters)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
// Example request: GET /api/v1/users?status=active&role=admin&search=john
HTTP Client Patterns
Production-Ready HTTP Client:
import ( "context" "net/http" "time" )
func NewHTTPClient() *http.Client { return &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, DisableKeepAlives: false, }, } }
// Making requests with context func fetchUser(ctx context.Context, userID string) (*User, error) { client := NewHTTPClient()
req, err := http.NewRequestWithContext(
ctx,
"GET",
fmt.Sprintf("https://api.example.com/users/%s", userID),
nil,
)
if err != nil {
return nil, fmt.Errorf("create request: %w", err)
}
req.Header.Set("Accept", "application/json")
req.Header.Set("Authorization", "Bearer "+getToken())
resp, err := client.Do(req)
if err != nil {
return nil, fmt.Errorf("execute request: %w", err)
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
return nil, fmt.Errorf("unexpected status: %d", resp.StatusCode)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
return nil, fmt.Errorf("decode response: %w", err)
}
return &user, nil
}
Retry Logic with Exponential Backoff:
import ( "context" "math" "time" )
type RetryConfig struct { MaxRetries int BaseDelay time.Duration MaxDelay time.Duration }
func DoWithRetry(ctx context.Context, cfg RetryConfig, fn func() error) error { var err error
for attempt := 0; attempt <= cfg.MaxRetries; attempt++ {
err = fn()
if err == nil {
return nil
}
if attempt < cfg.MaxRetries {
// Exponential backoff
delay := time.Duration(math.Pow(2, float64(attempt))) * cfg.BaseDelay
if delay > cfg.MaxDelay {
delay = cfg.MaxDelay
}
select {
case <-ctx.Done():
return ctx.Err()
case <-time.After(delay):
// Continue to next attempt
}
}
}
return fmt.Errorf("max retries exceeded: %w", err)
}
// Usage err := DoWithRetry(ctx, RetryConfig{ MaxRetries: 3, BaseDelay: 100 * time.Millisecond, MaxDelay: 2 * time.Second, }, func() error { return makeAPIRequest() })
Testing HTTP Handlers
Using httptest.Server:
import ( "net/http" "net/http/httptest" "testing" )
func TestGetUser(t *testing.T) { // Create test server ts := httptest.NewServer(http.HandlerFunc(getUserHandler)) defer ts.Close()
// Make request
resp, err := http.Get(ts.URL + "/users/123")
if err != nil {
t.Fatal(err)
}
defer resp.Body.Close()
// Assertions
if resp.StatusCode != http.StatusOK {
t.Errorf("expected status 200, got %d", resp.StatusCode)
}
var user User
if err := json.NewDecoder(resp.Body).Decode(&user); err != nil {
t.Fatal(err)
}
if user.ID != "123" {
t.Errorf("expected user ID 123, got %s", user.ID)
}
}
Testing Middleware:
func TestAuthMiddleware(t testing.T) { handler := http.HandlerFunc(func(w http.ResponseWriter, r http.Request) { userID := r.Context().Value("userID") if userID == nil { t.Error("userID not found in context") } w.WriteHeader(http.StatusOK) })
wrapped := AuthMiddleware(handler)
tests := []struct {
name string
token string
wantStatus int
}{
{"Valid token", "valid-token-123", http.StatusOK},
{"Missing token", "", http.StatusUnauthorized},
{"Invalid token", "invalid", http.StatusUnauthorized},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
req := httptest.NewRequest("GET", "/test", nil)
if tt.token != "" {
req.Header.Set("Authorization", tt.token)
}
rr := httptest.NewRecorder()
wrapped.ServeHTTP(rr, req)
if rr.Code != tt.wantStatus {
t.Errorf("expected status %d, got %d", tt.wantStatus, rr.Code)
}
})
}
}
Performance Optimization
Response Compression:
import ( "compress/gzip" "io" "net/http" "strings" )
type gzipResponseWriter struct { io.Writer http.ResponseWriter }
func (w gzipResponseWriter) Write(b []byte) (int, error) { return w.Writer.Write(b) }
func GzipMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { if !strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") { next.ServeHTTP(w, r) return }
w.Header().Set("Content-Encoding", "gzip")
gz := gzip.NewWriter(w)
defer gz.Close()
gzw := gzipResponseWriter{Writer: gz, ResponseWriter: w}
next.ServeHTTP(gzw, r)
})
}
Connection Pooling Configuration:
import ( "net" "net/http" "time" )
func NewProductionHTTPClient() *http.Client { return &http.Client{ Timeout: 30 * time.Second, Transport: &http.Transport{ Proxy: http.ProxyFromEnvironment, DialContext: (&net.Dialer{ Timeout: 30 * time.Second, KeepAlive: 30 * time.Second, }).DialContext, ForceAttemptHTTP2: true, MaxIdleConns: 100, MaxIdleConnsPerHost: 10, IdleConnTimeout: 90 * time.Second, TLSHandshakeTimeout: 10 * time.Second, ExpectContinueTimeout: 1 * time.Second, }, } }
Decision Tree Building HTTP API in Go? │ ├─ Simple API, few dependencies? → net/http (stdlib) │ ├─ Go 1.22+? → Use new ServeMux pattern routing │ └─ Go < 1.22? → Consider Chi for better routing │ ├─ Need stdlib compatibility + better ergonomics? → Chi │ └─ Great for: Middleware chains, route grouping │ ├─ Maximum performance priority? → Gin │ └─ Great for: JSON APIs, high throughput services │ ├─ Enterprise app with OpenAPI? → Echo │ └─ Great for: Type safety, comprehensive middleware │ └─ Team knows Express.js + need WebSockets? → Fiber └─ Note: Uses fasthttp, not stdlib-compatible
Common Pitfalls
Pitfall 1: Not Closing Response Bodies
// Bad: Memory leak resp, _ := http.Get(url) body, _ := io.ReadAll(resp.Body) // Never closed!
// Good: Always defer close resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close() body, err := io.ReadAll(resp.Body)
Pitfall 2: Not Using Context for Timeouts
// Bad: No timeout control resp, _ := http.Get(url)
// Good: Use context with timeout ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) defer cancel()
req, _ := http.NewRequestWithContext(ctx, "GET", url, nil) resp, err := client.Do(req)
Pitfall 3: Ignoring HTTP Status Codes
// Bad: Not checking status resp, _ := http.Get(url) defer resp.Body.Close() json.NewDecoder(resp.Body).Decode(&result)
// Good: Always check status resp, err := http.Get(url) if err != nil { return err } defer resp.Body.Close()
if resp.StatusCode != http.StatusOK { return fmt.Errorf("unexpected status: %d", resp.StatusCode) }
Pitfall 4: Not Reusing HTTP Clients
// Bad: Creates new client per request (no connection pooling) func makeRequest() { client := &http.Client{} resp, _ := client.Get(url) }
// Good: Reuse client for connection pooling var httpClient = &http.Client{ Timeout: 30 * time.Second, }
func makeRequest() { resp, _ := httpClient.Get(url) }
Related Skills golang-testing-strategies: Testing HTTP handlers and middleware golang-database-patterns: Integrating databases with HTTP APIs toolchains-typescript-frameworks-nodejs-backend: Comparison with Node.js patterns References Go net/http Documentation Chi Framework Gin Framework Echo Framework Fiber Framework Go HTTP Best Practices