Golang REST API Example [Without Framework]

The Go (Golang) standard library offers a vast amount of primitives that can be applied to a variety of problems. One of them is the HTTP ServeMux, which is a request multiplexer and allows us to map a set to requests into handlers. By the end of this article we are going to:


Using Go ServeMux

The ServeMux type is the main component of our API as it allows us to route set of requests into handlers based on the request pattern. Although the pattern matching is limited and we have to do a fair amount of work to map requests to handlers based on regular expressions and http methods, we can easily add the missing pieces.

Let’s see a basic example

type userHandler struct {}

func (h *userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    // all users request are going to be routed here
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/users/", &userHanlder{})
    http.ListenAndServe(":8080", mux)
}

Let’s add a switch statement to drive our requests to their respective handlers

var (
    listUserRe = regexp.MustCompile(`^\/users[\/]*$`)
    getUserRe  = regexp.MustCompile(`^\/users\/(\d+)$`)
    createUserRe = regexp.MustCompile(`^\/users[\/]*$`)
)
type userHandler struct {}

func (h *userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("content-type", "application/json")
    switch {
    case r.Method == http.MethodGet && listUserRe.MatchString(r.URL.Path):
        h.List(w, r)
        return
    case r.Method == http.MethodGet && getUserRe.MatchString(r.URL.Path):
        h.Get(w, r)
        return
    case r.Method == http.MethodPost && createUserRe.MatchString(r.URL.Path):
        h.Create(w, r)
        return
    default:
        notFound(w, r)
        return
    }
}

func main() {
    mux := http.NewServeMux()
    mux.Handle("/users/", &userHanlder{})
    http.ListenAndServe(":8080", mux)
}

Now we just need to implement the respective handlers for each request and add some storage. We are going to add a in-memory storage for this example, but you could add any type of storage that best fits your needs.

// user represents our REST resource
type user struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

// our in-memory datastore
// rememeber to guard map access with a mutex for concurrent access
type datastore struct {
    m map[string]user
    *sync.RWMutex
}

type userHandler struct {
    store *datastore
}

// Get is one of the request/response handlers and is responsible for
// returning a User given its ID
// it will parse the user id from within the URL Path in the request
func (h *userHandler) Get(w http.ResponseWriter, r *http.Request) {
    matches := getUserRe.FindStringSubmatch(r.URL.Path)
    if len(matches) < 2 {
        notFound(w, r)
        return
    }
    h.store.RLock()
    u, ok := h.store.m[matches[1]]
    h.store.RUnlock()
    if !ok {
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte("user not found"))
        return
    }
    jsonBytes, err := json.Marshal(u)
    if err != nil {
        internalServerError(w, r)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

JSON REST API final example

Here’s the final example, once we put together all our pieces

package main

import (
    "encoding/json"
    "net/http"
    "regexp"
    "sync"
)

var (
    listUserRe   = regexp.MustCompile(`^\/users[\/]*$`)
    getUserRe    = regexp.MustCompile(`^\/users\/(\d+)$`)
    createUserRe = regexp.MustCompile(`^\/users[\/]*$`)
)

type user struct {
    ID   string `json:"id"`
    Name string `json:"name"`
}

type datastore struct {
    m map[string]user
    *sync.RWMutex
}

type userHandler struct {
    store *datastore
}

func (h *userHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    w.Header().Set("content-type", "application/json")
    switch {
    case r.Method == http.MethodGet && listUserRe.MatchString(r.URL.Path):
        h.List(w, r)
        return
    case r.Method == http.MethodGet && getUserRe.MatchString(r.URL.Path):
        h.Get(w, r)
        return
    case r.Method == http.MethodPost && createUserRe.MatchString(r.URL.Path):
        h.Create(w, r)
        return
    default:
        notFound(w, r)
        return
    }
}

func (h *userHandler) List(w http.ResponseWriter, r *http.Request) {
    h.store.RLock()
    users := make([]user, 0, len(h.store.m))
    for _, v := range h.store.m {
        users = append(users, v)
    }
    h.store.RUnlock()
    jsonBytes, err := json.Marshal(users)
    if err != nil {
        internalServerError(w, r)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

func (h *userHandler) Get(w http.ResponseWriter, r *http.Request) {
    matches := getUserRe.FindStringSubmatch(r.URL.Path)
    if len(matches) < 2 {
        notFound(w, r)
        return
    }
    h.store.RLock()
    u, ok := h.store.m[matches[1]]
    h.store.RUnlock()
    if !ok {
        w.WriteHeader(http.StatusNotFound)
        w.Write([]byte("user not found"))
        return
    }
    jsonBytes, err := json.Marshal(u)
    if err != nil {
        internalServerError(w, r)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

func (h *userHandler) Create(w http.ResponseWriter, r *http.Request) {
    var u user
    if err := json.NewDecoder(r.Body).Decode(&u); err != nil {
        internalServerError(w, r)
        return
    }
    h.store.Lock()
    h.store.m[u.ID] = u
    h.store.Unlock()
    jsonBytes, err := json.Marshal(u)
    if err != nil {
        internalServerError(w, r)
        return
    }
    w.WriteHeader(http.StatusOK)
    w.Write(jsonBytes)
}

func internalServerError(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusInternalServerError)
    w.Write([]byte("internal server error"))
}

func notFound(w http.ResponseWriter, r *http.Request) {
    w.WriteHeader(http.StatusNotFound)
    w.Write([]byte("not found"))
}

func main() {
    mux := http.NewServeMux()
    userH := &userHandler{
        store: &datastore{
            m: map[string]user{
                "1": user{ID: "1", Name: "bob"},
            },
            RWMutex: &sync.RWMutex{},
        },
    }
    mux.Handle("/users", userH)
    mux.Handle("/users/", userH)

    http.ListenAndServe("localhost:8080", mux)
}

Here’s the source code https://play.golang.org/p/fbr87Ktb0kQ

Running and Testing our REST API Server

Try to save our example in a file and start your server

➜ go run server.go

And this is how our REST APIs works once completed ✨

➜ curl http://localhost:8080/users
[{"id":"1","name":"bob"}]

➜ curl http://localhost:8080/users/1
{"id":"1","name":"bob"}

➜ curl -X POST -H 'content-type: application/json' --data '{"id": "2", "name": "karen"}' http://localhost:8080/users
{"id":"2","name":"karen"}

➜ curl http://localhost:8080/users
[{"id":"1","name":"bob"},{"id":"2","name":"karen"}]

➜ curl http://localhost:8080/users/2
{"id":"2","name":"karen"}



Ready for your next Go Job? Submit your profile and get hired on Golang Cafe