Backend Development

Building RESTful Web APIs with Golang

Bima Adam Nugraha
March 10, 2024
18 min read
#Golang#REST API#Web Development#HTTP#Backend

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.sum

Conclusion

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.