Go's standard library provides excellent support for building web applications and APIs. With its performance, simplicity, and built-in concurrency, Go is an ideal choice for creating scalable RESTful services. In this guide, we'll walk through building a production-ready API from scratch.
Creating a Basic HTTP Server
Go's net/http package makes it incredibly easy to create a web server. Let's start with a simple example:
package main
import (
"encoding/json"
"log"
"net/http"
)
type Response struct {
Message string `json:"message"`
Status string `json:"status"`
}
func healthHandler(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
response := Response{
Message: "Server is running",
Status: "healthy",
}
json.NewEncoder(w).Encode(response)
}
func main() {
http.HandleFunc("/health", healthHandler)
log.Println("Server starting on :8080")
if err := http.ListenAndServe(":8080", nil); err != nil {
log.Fatal(err)
}
}Advanced Routing
While the standard library is powerful, for more complex routing, we can use popular libraries like Gorilla Mux or Chi. Here's an example using Gorilla Mux:
package main
import (
"encoding/json"
"log"
"net/http"
"github.com/gorilla/mux"
)
type User struct {
ID string `json:"id"`
Name string `json:"name"`
Email string `json:"email"`
}
var users = []User{
{ID: "1", Name: "John Doe", Email: "john@example.com"},
{ID: "2", Name: "Jane Smith", Email: "jane@example.com"},
}
func getUsers(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
func getUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
params := mux.Vars(r)
for _, user := range users {
if user.ID == params["id"] {
json.NewEncoder(w).Encode(user)
return
}
}
http.Error(w, "User not found", http.StatusNotFound)
}
func createUser(w http.ResponseWriter, r *http.Request) {
w.Header().Set("Content-Type", "application/json")
var user User
if err := json.NewDecoder(r.Body).Decode(&user); err != nil {
http.Error(w, err.Error(), http.StatusBadRequest)
return
}
users = append(users, user)
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(user)
}
func main() {
r := mux.NewRouter()
// API routes
api := r.PathPrefix("/api/v1").Subrouter()
api.HandleFunc("/users", getUsers).Methods("GET")
api.HandleFunc("/users/{id}", getUser).Methods("GET")
api.HandleFunc("/users", createUser).Methods("POST")
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}Middleware for Cross-Cutting Concerns
Middleware allows you to wrap your HTTP handlers with additional functionality like logging, authentication, and CORS handling.
Logging Middleware
Track all incoming requests with timing information.
func loggingMiddleware(next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
start := time.Now()
log.Printf(
"%s %s %s %s",
r.Method,
r.RequestURI,
r.RemoteAddr,
time.Since(start),
)
next.ServeHTTP(w, r)
})
}
// Usage
r.Use(loggingMiddleware)Authentication Middleware
Protect routes with JWT token validation.
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 here
if !isValidToken(token) {
http.Error(w, "Invalid token", http.StatusUnauthorized)
return
}
next.ServeHTTP(w, r)
})
}
// Usage on specific routes
protected := r.PathPrefix("/api/v1/admin").Subrouter()
protected.Use(authMiddleware)Database Integration
Let's integrate a PostgreSQL database using the popular GORM library for object-relational mapping:
package main
import (
"log"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
type User struct {
gorm.Model
Name string `json:"name"`
Email string `json:"email" gorm:"unique"`
}
func initDB() (*gorm.DB, error) {
dsn := "host=localhost user=postgres password=postgres dbname=myapp port=5432"
db, err := gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
return nil, err
}
// Auto-migrate schema
if err := db.AutoMigrate(&User{}); err != nil {
return nil, err
}
return db, nil
}
// Handler with database access
func getUsersHandler(db *gorm.DB) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
var users []User
if err := db.Find(&users).Error; err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(users)
}
}
func main() {
db, err := initDB()
if err != nil {
log.Fatal("Failed to connect to database:", err)
}
r := mux.NewRouter()
r.HandleFunc("/api/v1/users", getUsersHandler(db)).Methods("GET")
log.Println("Server starting on :8080")
log.Fatal(http.ListenAndServe(":8080", r))
}Best Practices
Use Context
Implement request context for cancellation and timeouts.
Error Handling
Return proper HTTP status codes and error messages.
Validation
Validate input data before processing requests.
CORS Configuration
Configure CORS properly for frontend integration.
Rate Limiting
Implement rate limiting to prevent abuse.
Structured Logging
Use structured logging for better observability.
Recommended Project Structure
Organize your Go API project with a clean architecture:
myapi/
├── cmd/
│ └── api/
│ └── main.go # Application entry point
├── internal/
│ ├── handlers/ # HTTP handlers
│ │ └── user_handler.go
│ ├── models/ # Data models
│ │ └── user.go
│ ├── repository/ # Database layer
│ │ └── user_repository.go
│ ├── service/ # Business logic
│ │ └── user_service.go
│ └── middleware/ # Custom middleware
│ └── auth.go
├── pkg/ # Public libraries
│ ├── config/
│ └── utils/
├── migrations/ # Database migrations
├── .env # Environment variables
├── go.mod
└── go.sumConclusion
Building RESTful APIs with Go is straightforward and rewarding. With its excellent standard library, rich ecosystem of third-party packages, and strong performance characteristics, Go is an outstanding choice for API development. Start with the basics, implement proper middleware, follow best practices, and you'll have a production-ready API that scales with your needs.