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: Usesync.RWMutexinstead ofsync.Mutexfor 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 andjson.NewEncoder(w)for responses. - Protect shared state with
sync.RWMutexfor 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.