JSTAcademy
0 XP
Dashboard
Crash Courses
Full REST API
60 min
PhD+200 XP
Crash Courses · PhD

Full REST API

In-memory CRUD API with routing, middleware, and error handling
60 min read+200 XP on completionCert: Go Crash Course
Tap any word in the text below to start reading from there.

Full REST API

Project Layout

api/
  main.go

Complete In-Memory REST API

package main

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

// ── Model ──────────────────────────────────────────────────────────────────

type Product struct {
    ID    int     `json:"id"`
    Name  string  `json:"name"`
    Price float64 `json:"price"`
}

// ── Store ──────────────────────────────────────────────────────────────────

type Store struct {
    mu       sync.RWMutex
    products map[int]Product
    nextID   int
}

func NewStore() *Store {
    s := &Store{products: make(map[int]Product), nextID: 1}
    s.products[1] = Product{ID: 1, Name: "Go T-Shirt", Price: 25.00}
    s.nextID = 2
    return s
}

func (s *Store) List() []Product {
    s.mu.RLock()
    defer s.mu.RUnlock()
    list := make([]Product, 0, len(s.products))
    for _, p := range s.products {
        list = append(list, p)
    }
    return list
}

func (s *Store) Get(id int) (Product, bool) {
    s.mu.RLock()
    defer s.mu.RUnlock()
    p, ok := s.products[id]
    return p, ok
}

func (s *Store) Create(p Product) Product {
    s.mu.Lock()
    defer s.mu.Unlock()
    p.ID = s.nextID
    s.nextID++
    s.products[p.ID] = p
    return p
}

func (s *Store) Update(id int, p Product) (Product, bool) {
    s.mu.Lock()
    defer s.mu.Unlock()
    if _, ok := s.products[id]; !ok {
        return Product{}, false
    }
    p.ID = id
    s.products[id] = p
    return p, true
}

func (s *Store) Delete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    if _, ok := s.products[id]; !ok {
        return false
    }
    delete(s.products, id)
    return true
}

// ── Helpers ────────────────────────────────────────────────────────────────

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

func idFromPath(path string) (int, bool) {
    parts := strings.Split(strings.Trim(path, "/"), "/")
    if len(parts) < 2 {
        return 0, false
    }
    id, err := strconv.Atoi(parts[len(parts)-1])
    return id, err == nil
}

// ── Handlers ───────────────────────────────────────────────────────────────

func productsHandler(store *Store) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        switch r.Method {
        case http.MethodGet:
            writeJSON(w, http.StatusOK, store.List())

        case http.MethodPost:
            var p Product
            if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
                http.Error(w, "bad request", http.StatusBadRequest)
                return
            }
            created := store.Create(p)
            writeJSON(w, http.StatusCreated, created)

        default:
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        }
    }
}

func productHandler(store *Store) http.HandlerFunc {
    return func(w http.ResponseWriter, r *http.Request) {
        id, ok := idFromPath(r.URL.Path)
        if !ok {
            http.Error(w, "invalid id", http.StatusBadRequest)
            return
        }

        switch r.Method {
        case http.MethodGet:
            p, found := store.Get(id)
            if !found {
                http.Error(w, "not found", http.StatusNotFound)
                return
            }
            writeJSON(w, http.StatusOK, p)

        case http.MethodPut:
            var p Product
            if err := json.NewDecoder(r.Body).Decode(&p); err != nil {
                http.Error(w, "bad request", http.StatusBadRequest)
                return
            }
            updated, found := store.Update(id, p)
            if !found {
                http.Error(w, "not found", http.StatusNotFound)
                return
            }
            writeJSON(w, http.StatusOK, updated)

        case http.MethodDelete:
            if !store.Delete(id) {
                http.Error(w, "not found", http.StatusNotFound)
                return
            }
            w.WriteHeader(http.StatusNoContent)

        default:
            http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
        }
    }
}

// ── Middleware ─────────────────────────────────────────────────────────────

func logging(next http.Handler) http.Handler {
    return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
        start := time.Now()
        next.ServeHTTP(w, r)
        log.Printf("%s %s %v", r.Method, r.URL.Path, time.Since(start))
    })
}

// ── Main ───────────────────────────────────────────────────────────────────

func main() {
    store := NewStore()
    mux := http.NewServeMux()

    mux.HandleFunc("/products", productsHandler(store))
    mux.HandleFunc("/products/", productHandler(store))

    server := &http.Server{
        Addr:         ":8080",
        Handler:      logging(mux),
        ReadTimeout:  5 * time.Second,
        WriteTimeout: 10 * time.Second,
    }

    fmt.Println("API running on http://localhost:8080")
    log.Fatal(server.ListenAndServe())
}

Test with curl

# List products
curl http://localhost:8080/products

# Create
curl -X POST -H "Content-Type: application/json" \
  -d '{"name":"Gopher Mug","price":12.99}' \
  http://localhost:8080/products

# Get one
curl http://localhost:8080/products/1

# Update
curl -X PUT -H "Content-Type: application/json" \
  -d '{"name":"Go Hoodie","price":45.00}' \
  http://localhost:8080/products/1

# Delete
curl -X DELETE http://localhost:8080/products/1
0%