toolchains/golang/web/SKILL.md
Go HTTP API development with net/http, Chi, Gin, Echo, and Fiber frameworks
npx skillsauth add bobmatnyc/claude-mpm-skills golang-http-frameworksInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
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:
Activate this skill when:
Use When:
Strengths:
Limitations:
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"`
}
Use When:
Strengths:
net/httpInstallation:
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})
}
Use When:
Strengths:
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",
})
}
Use When:
Strengths:
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})
}
Use When:
Strengths:
Limitations:
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)
}
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"`
}
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)
})
}
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))
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
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()
})
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)
}
})
}
}
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,
},
}
}
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
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)
}
development
Optimize web performance using Core Web Vitals, modern patterns (View Transitions, Speculation Rules), and framework-specific techniques
development
Best practices for documenting APIs and code interfaces, eliminating redundant documentation guidance per agent.
development
Comprehensive API design patterns covering REST, GraphQL, gRPC, versioning, authentication, and modern API best practices
development
Visual verification workflow for UI changes to accelerate code review and catch ...