GoGo · Lesson 7 of 8

Mini Project: HTTP Server

Go's standard library includes a production-quality HTTP server. No frameworks needed for the basics. Let's build one.

Go
package main

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

type Task struct {
    ID        int       `json:"id"`
    Text      string    `json:"text"`
    Done      bool      `json:"done"`
    CreatedAt time.Time `json:"created_at"`
}

type Store struct {
    mu     sync.Mutex
    tasks  []Task
    nextID int
}

func (s *Store) Add(text string) Task {
    s.mu.Lock()
    defer s.mu.Unlock()
    s.nextID++
    t := Task{ID: s.nextID, Text: text, Done: false, CreatedAt: time.Now()}
    s.tasks = append(s.tasks, t)
    return t
}

func (s *Store) List() []Task {
    s.mu.Lock()
    defer s.mu.Unlock()
    result := make([]Task, len(s.tasks))
    copy(result, s.tasks)
    return result
}

func (s *Store) Complete(id int) bool {
    s.mu.Lock()
    defer s.mu.Unlock()
    for i := range s.tasks {
        if s.tasks[i].ID == id {
            s.tasks[i].Done = true
            return true
        }
    }
    return false
}

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 main() {
    store := &Store{nextID: 0}

    http.HandleFunc("GET /tasks", func(w http.ResponseWriter, r *http.Request) {
        writeJSON(w, http.StatusOK, store.List())
    })

    http.HandleFunc("POST /tasks", func(w http.ResponseWriter, r *http.Request) {
        var body struct{ Text string `json:"text"` }
        if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
            http.Error(w, "Bad request", http.StatusBadRequest)
            return
        }
        task := store.Add(body.Text)
        writeJSON(w, http.StatusCreated, task)
    })

    http.HandleFunc("PUT /tasks/{id}/done", func(w http.ResponseWriter, r *http.Request) {
        id, err := strconv.Atoi(r.PathValue("id"))
        if err != nil {
            http.Error(w, "Invalid ID", http.StatusBadRequest)
            return
        }
        if !store.Complete(id) {
            http.Error(w, "Not found", http.StatusNotFound)
            return
        }
        writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
    })

    addr := ":8080"
    fmt.Printf("Server running on http://localhost%s\n", addr)
    log.Fatal(http.ListenAndServe(addr, nil))
}
Bash
go run main.go

# In another terminal:
curl -X POST http://localhost:8080/tasks -H "Content-Type: application/json" -d '{"text":"Learn Go"}'
curl http://localhost:8080/tasks
curl -X PUT http://localhost:8080/tasks/1/done
◆ Note
sync.Mutex protects the task list from concurrent access. Go servers handle each request in its own goroutine, so shared state must be protected. This is a simple example — production code would use a database.