Back to Go Examples

Project: HTTP URL Shortener

A working JSON REST service using only the standard library — no frameworks, no dependencies, no boilerplate.

Contents

  1. What You'll Build
  2. The API
  3. The Complete Code
  4. How It Works
  5. Run It
  6. Extensions

1. What You'll Build

A small HTTP service that hands out short codes for long URLs. POST /shorten with a JSON body creates a code; GET /<code> redirects to the original URL; GET / lists everything stored.

This is a great showcase for Go because the entire thing fits in 90 lines of standard-library code — no framework, no router library, no ORM. Just net/http, encoding/json, and a map.

2. The API

# Create a short code
$ curl -s -X POST localhost:8080/shorten \
    -H 'Content-Type: application/json' \
    -d '{"url": "https://bozcode.com"}'
{"code":"a3f9k2","short_url":"http://localhost:8080/a3f9k2","target":"https://bozcode.com"}

# Follow the short URL
$ curl -i localhost:8080/a3f9k2
HTTP/1.1 302 Found
Location: https://bozcode.com

# List everything
$ curl -s localhost:8080/
{"a3f9k2":"https://bozcode.com"}

3. The Complete Code

package main

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

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

func newStore() *store {
    return &store{byCode: make(map[string]string)}
}

func randomCode() string {
    b := make([]byte, 5)
    rand.Read(b)
    return strings.ToLower(base64.RawURLEncoding.EncodeToString(b))
}

type shortenReq struct {
    URL string `json:"url"`
}
type shortenResp struct {
    Code     string `json:"code"`
    ShortURL string `json:"short_url"`
    Target   string `json:"target"`
}

func (s *store) shorten(w http.ResponseWriter, r *http.Request) {
    if r.Method != http.MethodPost {
        http.Error(w, "POST only", http.StatusMethodNotAllowed)
        return
    }
    var req shortenReq
    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 != "http" && u.Scheme != "https") {
        http.Error(w, "url must be http or https", http.StatusBadRequest)
        return
    }

    code := randomCode()
    s.mu.Lock()
    s.byCode[code] = req.URL
    s.mu.Unlock()

    w.Header().Set("Content-Type", "application/json")
    json.NewEncoder(w).Encode(shortenResp{
        Code:     code,
        ShortURL: "http://" + r.Host + "/" + code,
        Target:   req.URL,
    })
}

func (s *store) resolve(w http.ResponseWriter, r *http.Request) {
    code := strings.TrimPrefix(r.URL.Path, "/")

    if code == "" {  // list everything at /
        s.mu.RLock()
        defer s.mu.RUnlock()
        w.Header().Set("Content-Type", "application/json")
        json.NewEncoder(w).Encode(s.byCode)
        return
    }

    s.mu.RLock()
    target, ok := s.byCode[code]
    s.mu.RUnlock()
    if !ok {
        http.NotFound(w, r)
        return
    }
    http.Redirect(w, r, target, http.StatusFound)
}

func main() {
    s := newStore()

    http.HandleFunc("/shorten", s.shorten)
    http.HandleFunc("/", s.resolve)

    log.Println("url shortener listening on :8080")
    log.Fatal(http.ListenAndServe(":8080", nil))
}

4. How It Works

  1. The store is a struct + mutex. Many goroutines can be serving HTTP requests at once, so reads and writes to the map must be protected. sync.RWMutex lets many readers in but only one writer at a time — perfect for a read-heavy workload like URL resolution.
  2. Random codes come from crypto/rand. We grab 5 random bytes, base64-encode them with the URL-safe alphabet, lower-case the result. Short, unpredictable, URL-safe.
  3. JSON in and out. The struct tags `json:"url"` tell encoding/json what field names to look for. json.NewDecoder(r.Body).Decode(&req) reads and parses the body into your struct in one line.
  4. Validation. url.ParseRequestURI plus a scheme check rejects malformed inputs. Always validate user input before you store it.
  5. Routing. http.HandleFunc("/shorten", ...) and http.HandleFunc("/", ...) is the entire router. The catch-all at / handles both the listing (when path is empty) and the redirect (when there's a code).
  6. Redirects. http.Redirect(w, r, target, 302) writes the proper Location: header and status line in one call.
No web framework. No ORM. No middleware library. The Go standard library shipped a production-grade HTTP server in 2012 and it's never needed replacing.

5. Run It

$ mkdir shorty && cd shorty
$ go mod init shorty
# paste the code into main.go
$ go run main.go
url shortener listening on :8080

# in another terminal:
$ curl -X POST localhost:8080/shorten \
    -H 'Content-Type: application/json' \
    -d '{"url":"https://bozcode.com"}'

Copy the code from the response, then paste http://localhost:8080/<code> in your browser. You should land on the original URL.

6. Extensions

Once you've built one of these, you've built a thousand. Every small Go web service follows the same shape: a store struct, handler methods that take (w, r), JSON in and out, and http.ListenAndServe at the bottom of main.