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:
- See how to implement a simple REST API just with the Go standard library
- Use the Go ServeMux type to route requests to handlers
- Use minimal scaffolding to implement a basic JSON REST API
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"}