002 Project 2: URL Shortener API

002 Build a URL Shortener API

You will build a small HTTP service with two endpoints:

  • POST /shorten -> create short URL
  • GET /:code -> redirect to original URL
Request flow

client -> POST /shorten -> store (code -> url) -> return short URL
client -> GET /abc123  -> lookup code -> 302 redirect

Full main.go

package main

import (
    "crypto/rand"
    "encoding/base64"
    "encoding/json"
    "fmt"
    "log"
    "net/http"
    "net/url"
    "strings"
    "sync"
)

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

type shortenRequest struct {
    URL string `json:"url"`
}

type shortenResponse struct {
    Code string `json:"code"`
    URL  string `json:"short_url"`
}

func newCode(n int) string {
    b := make([]byte, n)
    if _, err := rand.Read(b); err != nil {
        panic(err)
    }
    return strings.TrimRight(base64.RawURLEncoding.EncodeToString(b), "=")
}

func main() {
    s := &store{data: make(map[string]string)}
    mux := http.NewServeMux()

    mux.HandleFunc("POST /shorten", func(w http.ResponseWriter, r *http.Request) {
        var req shortenRequest
        if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
            http.Error(w, "invalid json", http.StatusBadRequest)
            return
        }

        u, err := url.ParseRequestURI(req.URL)
        if err != nil || u.Scheme == "" || u.Host == "" {
            http.Error(w, "invalid url", http.StatusBadRequest)
            return
        }

        code := newCode(5)
        s.mu.Lock()
        s.data[code] = req.URL
        s.mu.Unlock()

        resp := shortenResponse{
            Code: code,
            URL:  fmt.Sprintf("http://%s/%s", r.Host, code),
        }
        w.Header().Set("Content-Type", "application/json")
        _ = json.NewEncoder(w).Encode(resp)
    })

    mux.HandleFunc("GET /{code}", func(w http.ResponseWriter, r *http.Request) {
        code := r.PathValue("code")
        s.mu.RLock()
        longURL, ok := s.data[code]
        s.mu.RUnlock()
        if !ok {
            http.NotFound(w, r)
            return
        }
        http.Redirect(w, r, longURL, http.StatusFound)
    })

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

Run and Test

go run .

curl -s -X POST localhost:8080/shorten \
  -H 'content-type: application/json' \
  -d '{"url":"https://go.dev/doc/"}'

curl -i localhost:8080/<code-from-response>

What to Extend

  1. Persistent storage (BoltDB/Postgres).
  2. Expiry timestamps.
  3. Rate limiting middleware.

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.