advanced Step 14 of 15

Building a REST API

Go Programming

Building a REST API

Building a REST API in Go combines HTTP handling, JSON serialization, routing, and database access into a cohesive web service. Go's performance, built-in concurrency, and simple deployment (single binary) make it an excellent choice for microservices and APIs. In this lesson, you will build a complete CRUD API with proper error handling, input validation, and a clean project structure.

REST API Implementation

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "strconv"
    "sync"
)

type Todo struct {
    ID        int    `json:"id"`
    Title     string `json:"title"`
    Completed bool   `json:"completed"`
}

type TodoStore struct {
    mu     sync.RWMutex
    todos  []Todo
    nextID int
}

func NewTodoStore() *TodoStore {
    return &TodoStore{nextID: 1}
}

func (s *TodoStore) GetAll() []Todo {
    s.mu.RLock()
    defer s.mu.RUnlock()
    return append([]Todo{}, s.todos...)
}

func (s *TodoStore) Create(title string) Todo {
    s.mu.Lock()
    defer s.mu.Unlock()
    todo := Todo{ID: s.nextID, Title: title}
    s.nextID++
    s.todos = append(s.todos, todo)
    return todo
}

func (s *TodoStore) Update(id int, title string, completed bool) (Todo, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    for i := range s.todos {
        if s.todos[i].ID == id {
            s.todos[i].Title = title
            s.todos[i].Completed = completed
            return s.todos[i], true
        }
    }
    return Todo{}, false
}

func (s *TodoStore) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    for i, t := range s.todos {
        if t.ID == id {
            s.todos = append(s.todos[:i], s.todos[i+1:]...)
            return true
        }
    }
    return false
}

var store = NewTodoStore()

func respondJSON(w http.ResponseWriter, status int, data interface{}) {
    w.Header().Set("Content-Type", "application/json")
    w.WriteHeader(status)
    json.NewEncoder(w).Encode(data)
}

func handleTodos(w http.ResponseWriter, r *http.Request) {
    switch r.Method {
    case "GET":
        respondJSON(w, http.StatusOK, store.GetAll())
    case "POST":
        var input struct{ Title string `json:"title"` }
        json.NewDecoder(r.Body).Decode(&input)
        if input.Title == "" {
            respondJSON(w, 400, map[string]string{"error": "title required"})
            return
        }
        todo := store.Create(input.Title)
        respondJSON(w, 201, todo)
    }
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/api/todos", handleTodos)

    log.Println("Server starting on :8080")
    log.Fatal(http.ListenAndServe(":8080", mux))
}
Pro tip: Use sync.RWMutex instead of sync.Mutex for read-heavy workloads — it allows multiple concurrent readers while still providing exclusive access for writers. In production, replace the in-memory store with a database, but the handler patterns remain the same.

Key Takeaways

  • REST APIs map HTTP methods to CRUD operations: GET (read), POST (create), PUT (update), DELETE (delete).
  • Use json.NewDecoder(r.Body) to parse request bodies and json.NewEncoder(w) for responses.
  • Protect shared state with sync.RWMutex for concurrent access safety.
  • Return appropriate HTTP status codes: 200 (OK), 201 (Created), 404 (Not Found), 400 (Bad Request).
  • Go's single-binary deployment makes it ideal for containerized microservices.