Building a REST API in Go

REST Server in Go using the standard library

When ever we pick up a new language,the most obvious question that arises is, what if we want to create a REST API, what framework shall we use, but that question with Go can be answered like “we can use the standard library provided by Go to develop REST APIs”.This post and the forthcoming ones will try to see the various ways we can develop REST APIs using Go and some of the frameworks that are in place and try to see the differences between the various approaches.

Code

The complete source can be found here

The Model

Just to keep things simple we will be building a simple address book where we will be storing a Person’s name,email,mobile,address and created_at fields and it will present the following REST APIs to the client:

POST /contact/ : Create a new Contact and return the email address
GET  /contact/<emailid>: Return a single contact by the emailid
GET  /contact/ : Return all the Contacts
DELETE /contact/<emailid>: Delete the contact by emailid
GET /createdAt/<yy>/<mm>/<dd>: Return list of contacts created on the date specified

The data will be encoded in JSON. Let’s start by desinging the Data layer. The code for this is under the contacts/contacts.go file. We will use an in-memory store for storing the contact details which will look like:

package contacts

import (
	"sync"
	"time"
)

type Contacts struct {
	
	Name      string    `json:"name"`
	Email     string    `json:"email"`
	Address   string    `json:"address"`
	Mobile    string    `json:"mobile"`
	CreatedAt time.Time `json:"createdAt"`
}

// Creating an In memory Address book that is safe to access concurrently
type AddressBook struct {
	sync.Mutex

	contacts map[string]Contacts
}

func New() *AddressBook {
	ab := &AddressBook{}
	ab.contacts = make(map[string]Contacts)

	return ab
}

Next,in the same file we will be adding the following functions:

  1. CreateContact(name string, email string, address string, mobile string, createdAt time.Time) string
  2. GetContact(email string) (Contacts, error)
  3. DeleteContact(email string) error
  4. DeleteAllContacts() error
  5. GetAllContacts() []Contacts
  6. GetContactByCreatedDate(year int, month time.Month, day int) []Contacts
// Create a new Contact in the address book
func (ab *AddressBook) CreateContact(name string, email string, address string, mobile string, createdAt time.Time) string {
	ab.Lock()

	defer ab.Unlock()

	contacts := Contacts{

		Name:      name,
		Email:     email,
		Address:   address,
		Mobile:    mobile,
		CreatedAt: createdAt,
	}
	ab.contacts[email] = contacts

	return email

}

// Retrieve a Contact by Emailid from the store and return an Error if it does
//exist
func (ab *AddressBook) GetContact(email string) (Contacts, error) {
	ab.Lock()

	defer ab.Unlock()
	c, ok := ab.contacts[email]

	if ok {
		return c, nil
	} else {
		return Contacts{}, fmt.Errorf("Contact with email=%s not found", email)
	}
}

//Delete a Contact with the give email,returns error if no match if found
func (ab *AddressBook) DeleteContact(email string) error {
	ab.Lock()
	defer ab.Unlock()

	if _, ok := ab.contacts[email]; !ok {
		return fmt.Errorf("Contact with Email=%s not found", email)
	}
	delete(ab.contacts, email)
	return nil
}

//Delete all Contacts
func (ab *AddressBook) DeleteAllContacts() error {
	ab.Lock()

	defer ab.Unlock()

	ab.contacts = make(map[string]Contacts)

	return nil
}

//Retrieve all Contacts
func (ab *AddressBook) GetAllContacts() []Contacts {
	ab.Lock()

	defer ab.Unlock()

	contacts := make([]Contacts, len(ab.contacts))
	for _, contact := range ab.contacts {
		contacts = append(contacts, contact)
	}
	return contacts
}

//Get Contacts by CreatedAt Date
func (ab *AddressBook) GetContactByCreatedDate(year int, month time.Month, day int) []Contacts {
	ab.Lock()

	defer ab.Unlock()

	var contacts []Contacts

	for _, contact := range ab.contacts {
		y, m, d := contact.CreatedAt.Date()
		if y == year && m == month && d == day {
			contacts = append(contacts, contact)
		}
	}
	return contacts
}

The Address Book is implemented using a simple in memory store by storing the contacts in a map[string]contacts, where each contact is stored against an email address.However,in a real world this could be a permanent storage like Postgres or MongoDB.

Setting up the Server

The main function is where we will define the Server to initialize the store,add handler functions for each type of tasks we defined above and define the routing capability.First let’s see how we can start our server run on port 8000:

func main() {
	mux := http.NewServeMux()
	server := NewContactServer()
	mux.HandleFunc("/contact/", server.contactHandler)
	mux.HandleFunc("/createdAt/", server.createdAtHandler)
	log.Fatal((http.ListenAndServe("localhost:8000", mux)))
}

NewContactServer is a just struct which initializes the AddressBook which is defined as:

type contactServer struct {
	store *contacts.AddressBook
}

func NewContactServer() *contactServer {
	store := contacts.New()
	return &contactServer{store: store}
}

Routing and Handlers

For handling the routing we will be using the basic multiplexer provided by the net/http package and we defined the following routes:

mux.HandleFunc("/contact/", server.contactHandler)
mux.HandleFunc("/createdAt/", server.createdAtHandler)

This very bare bones and it’s both good and bad. Good in the sense that it’s simple,easy to understand and much more control in the way we want it. The bad part is that it makes pattern matching really difficult as we have to handle all the possible URL patterns ourselves.Let’s have a look at the contactHandler:

func (ab *contactServer) contactHandler(w http.ResponseWriter, req *http.Request) {
	if req.URL.Path == "/contact/" {

		if req.Method == http.MethodPost {
			ab.createContactHandler(w, req)
		} else if req.Method == http.MethodGet {
			ab.getAllContactsHandler(w, req)
		} else if req.Method == http.MethodDelete {
			ab.deleteAllContactsHandler(w, req)
		} else {
			http.Error(w, fmt.Sprintf("expect method GET, DELETE or POST at /contact/, got %v", req.Method), http.StatusMethodNotAllowed)
			return
		}
	} else {

		path := strings.Trim(req.URL.Path, "/")
		pathParts := strings.Split(path, "/")
		if len(pathParts) < 2 {
			http.Error(w, "expect /contact/<emailid> in contact handler", http.StatusBadRequest)
			return
		}
		emailid := pathParts[1]

		if req.Method == http.MethodDelete {
			ab.deleteContactHandler(w, req, emailid)
		} else if req.Method == http.MethodGet {
			ab.getContactHandler(w, req, emailid)
		} else {
			http.Error(w, fmt.Sprintf("expect method GET or DELETE at /contact/<emailid>, got %v", req.Method), http.StatusMethodNotAllowed)
			return
		}
	}
}

Under the contact handler we are doing the following:

  1. We are first matching the URL path */contact/.
  2. Than we are checking the type of request,which can be POST,GET or DELETE and than invoking the respective handler.For example here is the handler to create a contact:
func (cs *contactServer) createContactHandler(w http.ResponseWriter, req *http.Request) {
	log.Printf("handling contact create at %s\n", req.URL.Path)

	//Used Internally to converto to/from JSON
	type Request struct {
		Name      string    `json:"name"`
		Email     string    `json:"email"`
		Address   string    `json:"address"`
		Mobile    string    `json:"mobile"`
		CreatedAt time.Time `json:"createdAt"`
	}

	type Response struct {
		Email string `json:"email"`
	}

	// Enforce a JSON Content-Type.
	contentType := req.Header.Get("Content-Type")
	mediatype, _, err := mime.ParseMediaType(contentType)
	if err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}
	if mediatype != "application/json" {
		http.Error(w, "expect application/json Content-Type", http.StatusUnsupportedMediaType)
		return
	}

	dec := json.NewDecoder(req.Body)
	dec.DisallowUnknownFields()
	var rs Request
	if err := dec.Decode(&rs); err != nil {
		http.Error(w, err.Error(), http.StatusBadRequest)
		return
	}

	email := cs.store.CreateContact(rs.Name, rs.Email, rs.Address, rs.Mobile, time.Now())
	js, err := json.Marshal(Response{Email: email})
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}

Here we are unmarshalling the JSON to an internal struct,creating a contact using the CreateContact method and than returning the emailid as a response.

  1. In the else part,which will be handled if we pass the emailid in the URL as /contact/, this will fetch the contact of the specified emailid:
func (cs *contactServer) getContactHandler(w htfunc (cs *contactServer) getContactHandler(w http.ResponseWriter, req *http.Request, emailid string) {
	log.Printf("handling get contact at %s\n", req.URL.Path)

	contact, err := cs.store.GetContact(emailid)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}

	js, err := json.Marshal(contact)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}tp.ResponseWriter, req *http.Request, emailid string) {
	log.Printf("handling get contact at %s\n", req.URL.Path)

	contact, err := cs.store.GetContact(emailid)
	if err != nil {
		http.Error(w, err.Error(), http.StatusNotFound)
		return
	}

	js, err := json.Marshal(contact)
	if err != nil {
		http.Error(w, err.Error(), http.StatusInternalServerError)
		return
	}
	w.Header().Set("Content-Type", "application/json")
	w.Write(js)
}

Improvements

The basic approach to creating REST APIs using Go’s standard library seems to work and is simple. There are some repetitive tasks like rendering a JSON which desereves to be in it’s own function to improve re-usability.We can also make use of a third party library to improve on the routing and pattern matching. All this will be covered in the next post.