A working JSON REST service using only the standard library — no frameworks, no dependencies, no boilerplate.
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.
# 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"}
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))
}
sync.RWMutex lets many readers in but only one writer at a time — perfect for a read-heavy workload like URL resolution.crypto/rand. We grab 5 random bytes, base64-encode them with the URL-safe alphabet, lower-case the result. Short, unpredictable, URL-safe.`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.url.ParseRequestURI plus a scheme check rejects malformed inputs. Always validate user input before you store it.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).http.Redirect(w, r, target, 302) writes the proper Location: header and status line in one call.$ 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.
store.json on every write, load it at startup. (Or graduate to SQLite with the database/sql package.)"alias" in the POST body so users can claim memorable-code.(url, expiresAt) pairs and reject lookups past the expiration. A goroutine running time.NewTicker(1 * time.Minute) can sweep expired entries.GET /stats/<code>.golang.org/x/time/rate./ alongside the API. Use html/template for safe rendering.(w, r), JSON in and out, and http.ListenAndServe at the bottom of main.