Why Go for APIs?
Go's simplicity, performance, and excellent standard library make it an ideal choice for building production-grade APIs. Unlike heavier frameworks in other languages, Go gives you fine-grained control over every aspect of your HTTP server.
Project Structure
A well-organized Go API project should separate concerns clearly:
api/
├── cmd/
│ └── server/
│ └── main.go
├── internal/
│ ├── handler/
│ ├── middleware/
│ ├── model/
│ └── repository/
├── go.mod
└── go.sumRouting with Chi
While the standard net/http package works fine, Chi provides a lightweight, composable router:
r := chi.NewRouter()
r.Use(middleware.Logger)
r.Use(middleware.Recoverer)
r.Route("/api/v1", func(r chi.Router) {
r.Get("/users", handler.ListUsers)
r.Post("/users", handler.CreateUser)
r.Get("/users/{id}", handler.GetUser)
})Error Handling
Consistent error responses are critical for API consumers:
type APIError struct {
Code int `json:"code"`
Message string `json:"message"`
}
func writeError(w http.ResponseWriter, status int, msg string) {
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(status)
json.NewEncoder(w).Encode(APIError{
Code: status,
Message: msg,
})
}Middleware Chain
Build a composable middleware chain for cross-cutting concerns:
- Authentication — validate JWT tokens
- Rate limiting — protect against abuse
- CORS — handle cross-origin requests
- Logging — request/response logging
- Recovery — catch panics gracefully
Testing
Use Go's built-in httptest package for integration tests:
func TestGetUser(t *testing.T) {
req := httptest.NewRequest("GET", "/api/v1/users/1", nil)
w := httptest.NewRecorder()
router.ServeHTTP(w, req)
assert.Equal(t, 200, w.Code)
}Conclusion
Go's philosophy of simplicity and explicitness leads to APIs that are easy to understand, maintain, and scale. The combination of Chi for routing, clean error handling, and composable middleware gives you a production-ready foundation.
Agamya Samuel
Software Developer & Cloud Architect