020 Project 20: KV HTTP Store

020 Build a Key-Value HTTP Store

A production-style mini service with CRUD endpoints and periodic snapshot persistence.

HTTP API <-> in-memory map + mutex <-> snapshot.json

Full main.go

package main

import (
    "encoding/json"
    "log"
    "net/http"
    "os"
    "sync"
    "time"
)

type store struct {
    mu sync.RWMutex
    m  map[string]string
}

func (s *store) load(path string) {
    b, err := os.ReadFile(path)
    if err != nil {
        return
    }
    s.mu.Lock()
    defer s.mu.Unlock()
    _ = json.Unmarshal(b, &s.m)
}

func (s *store) save(path string) error {
    s.mu.RLock()
    defer s.mu.RUnlock()
    b, err := json.MarshalIndent(s.m, "", "  ")
    if err != nil {
        return err
    }
    return os.WriteFile(path, b, 0o644)
}

func main() {
    s := &store{m: map[string]string{}}
    snap := "snapshot.json"
    s.load(snap)

    go func() {
        t := time.NewTicker(10 * time.Second)
        defer t.Stop()
        for range t.C {
            _ = s.save(snap)
        }
    }()

    mux := http.NewServeMux()
    mux.HandleFunc("PUT /kv/{key}", func(w http.ResponseWriter, r *http.Request) {
        key := r.PathValue("key")
        var body struct{ Value string `json:"value"` }
        if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
            http.Error(w, "bad json", http.StatusBadRequest)
            return
        }
        s.mu.Lock()
        s.m[key] = body.Value
        s.mu.Unlock()
        w.WriteHeader(http.StatusNoContent)
    })

    mux.HandleFunc("GET /kv/{key}", func(w http.ResponseWriter, r *http.Request) {
        key := r.PathValue("key")
        s.mu.RLock()
        v, ok := s.m[key]
        s.mu.RUnlock()
        if !ok {
            http.NotFound(w, r)
            return
        }
        _ = json.NewEncoder(w).Encode(map[string]string{"key": key, "value": v})
    })

    mux.HandleFunc("DELETE /kv/{key}", func(w http.ResponseWriter, r *http.Request) {
        key := r.PathValue("key")
        s.mu.Lock()
        delete(s.m, key)
        s.mu.Unlock()
        w.WriteHeader(http.StatusNoContent)
    })

    mux.HandleFunc("GET /kv", func(w http.ResponseWriter, r *http.Request) {
        s.mu.RLock()
        defer s.mu.RUnlock()
        _ = json.NewEncoder(w).Encode(s.m)
    })

    log.Println("listening :8081")
    log.Fatal(http.ListenAndServe(":8081", mux))
}

Run

go run .
curl -X PUT localhost:8081/kv/name -d '{"value":"king"}' -H 'content-type: application/json'
curl localhost:8081/kv/name

Step-by-Step Explanation

  1. Define request and response contracts first.
  2. Validate inbound input before doing any state changes.
  3. Keep handler logic short and move reusable logic into helper functions.
  4. Add timeouts and clear error paths.
  5. Return consistent responses and status codes.

Code Anatomy

  • Handlers parse input, call domain logic, write response.
  • Shared state uses synchronization where needed.
  • Transport concerns stay separate from business rules.

Learning Goals

  • Build reliable service endpoints in Go.
  • Understand API ergonomics and operational safety.
  • Prepare code structure for tests and persistence later.