Microservices in Golang, pt2 - REST architecture

We’ve learned how to create the very simple service with Golang and we’ve also restructured the service by using patterns that will use when we move forward.

In this article we’ll look into Restful service, or REpresentational State Transfer that makes it easier for systems to communicate with each other; RESTful systems are characterized by being stateless and separate the concerns of client and server; RESTful service are the most common service architecure in use. A good read regarding the subject (designing REST APIs for HTTP) can be found here .

Briefly, REST defines 6 architectural constraints which make any web service a RESTful API.

  • Uniform interface - along with the data, the server responses should also announce available actions and resources (what was queried, how the data is structured, how to retrieve related objects, if there is more related data based in the response lenght or pagination). So, REST is defined by four interface constraints: identification of resources; manipulation of resources through representations; self-descriptive messages; and, hypermedia as the engine of application state

  • Client–server - the implementation of the client and the implementation of the server can be done independently without each knowing about the other

  • Stateless - the server does not need to know anything about what state the client is in and vice versa

  • Cacheable - caching is the act of storing the server response in the client, so that a client does not need to make a server request for the same resource repeatedly. A server response should have information about how caching is to be done, so that a client caches the response for a time-period or never caches the server response

  • Layered system - every layer should have a single high-level purpose and deal only with that, it is about separation of concerns. One of the most common architectures is the so-called 3-tier architecture, where you have data access, business logic and presentation. Services could be added as a separate layer, most common between business logic and presentation.

  • Code on demand (optional) - this can be ignored for now, since in the old days we had Java applets, etc.

Terminologies#

Resource is an object or representation of an entity, which has some associated data with it and there can be set of methods to operate on. For example Products, Shops and Cities are resources.

Collections are set of resources, for example Products is the collection of Product resource.

URL (Uniform Resource Locator) is a path through which a resource can be located and some actions can be performed on it.

Actions is the intended operation to be performed in the resource, such as add, update or delete.

Making requests#

REST requires that a client make a request to the server in order to retrieve or modify data on the server, generaly consiting of:

  • an HTTP verb, which defines what kind of operation to perform
  • a header, which allows the client to pass along information about the request
  • a path to a resource
  • an optional message body containing data

HTTP verbs#

GET - the GET method requests a specific resource (by id) or a collection of resources. Requests using GET should only retrieve data

POST - the POST method is used to create a new resource. More specifically submit an entity to the specified resource, often causing a change in state or side effects on the server

PUT - the PUT method updates a specific resource (by id); replacing all current representations of the target resource with the request payload

DELETE - the DELETE request method deletes the specified resource, removes a specific resource by id

HEAD - the HEAD method asks for a response identical to that of a GET request, but without the response body

OPTIONS - the OPTIONS method is used to describe the communication options for the target resource

PATCH - the PATCH method is used to apply partial modifications to a resource

TRACE - the TRACE method performs a message loop-back test along the path to the target resource

When working in systems, you might find that these verbs are not always followed strictly. But this should provide you a guideline to what expect from each of these HTTP requests against a service endpoint.

Headers and parameters#

The HTTP headers are an important part of an service API request and response as they represent the meta-data associated with the service API request and response. Generally, the headers carry information for request or response body, request authorization, response caching, response cookies, and more.

We have to set the request headers when we are sending requests to a service API, and assert against the response to ensure the correct headers are being returned.

For example, a client accessing a resource with id 10 in an Products resource on a server might send a GET request like this:

GET /products/10
Accept: application/json

The Accept header field in this case says that the client accepts the content in application/json.

Path#

Requests must contain a path to a resource that the operation should be performed on. A Path comprises an HTTP verb and a URL path that, when exposed, is combined with the base path of the API. For example in the url https://jsonplaceholder.typicode.com/posts/1/comments, we have the path:

/posts/1/comments

Paths should only contain the information required to locate a resource with the degree of specificity needed. When looking for a list or collection of a resource, the id is not always necessary.

HTTP Status code#

HTTP response status codes indicate whether a specific HTTP request has been successfully completed. Responses are grouped in five classes:

  • Informational responses (100–199)
  • Successful responses (200–299)
  • Redirects (300–399),
  • Client errors (400–499)
  • Server errors (500–599)

You can find more about it by reading the HTTP response status codes doc.

Enough with theory and let’s get started writing our first REST API service in Golang.

Building a simple REST API#

Hopefully you’ve follow the part 1 , as I’ll be copying the structure, as the base for our REST service.

If your project is already in version control, you can simply run:

go mod init

Or you can supply the module path manually:

go mod init example.com/go-intro-microservices-pt2

This command will create go.mod file which both defines projects requirements and locks dependencies to their correct versions (if you’ve worked with nodejs, it’s similar to package.json and the package-lock.json put together).

It’s recommended to use a module path that corresponds to a repository you plan or will publish your module to, so when you do, go get will be able to automatically fetch, build and install your module. For example you may choose a module path github.com/punkbit/myPackage, so when you publish your module, everyone can get it by simply using import github.com/punkbit/myPackage in their app. More on modules can be found in the wiki .

So, let’s get started by creating a new file main.go and copy the content from our previously built microservice and keeping the Hello Handler only.

If you’re not familiar with the structure provided here as a base, it’s strongly recommended to check the part 1 .

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"example.com/go-intro-microservices/handlers"
)

func main() {
	l := log.New(os.Stdout, "rest-api", log.LstdFlags)

	hh := handlers.NewHello(l)

	sm := http.NewServeMux()

	sm.Handle("/", hh)

	s := &http.Server{
		Addr:         ":9000",
		Handler:      sm,
		IdleTimeout:  120 * time.Second,
		ReadTimeout:  1 * time.Second,
		WriteTimeout: 1 * time.Second,
	}

	go func() {
		err := s.ListenAndServe()
		if err != nil {
			l.Fatal(err)
		}
	}()

	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, os.Interrupt)
	signal.Notify(sigChan, os.Kill)

	sig := <-sigChan
	l.Println("Terminate received, gracefully shuttingdown...", sig)

	tc, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	s.Shutdown(tc)
}

Create the directory handlers from your project root directory where we’ll keep the Handlers; and the hello.go file.

mkdir ./handlers && touch ./handlers/hello.go

The content for the hello.go should be along the lines of:

package handlers

import (
	"log"
	"net/http"
)

type Hello struct {
	l *log.Logger
}

func NewHello(l *log.Logger) *Hello {
	return &Hello{l}
}

func (h *Hello) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	h.l.Println("Hello world!")

	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusOK)
	rw.Write([]byte(`{"message": "hello world"}`))
}

Now, if you run the program:

go run main.go

You can run curl:

curl localhost:9000

And it should output:

{"message": "hello world"}

In the ServeHTTP method we’ve set the http status code 200 that means success. We se the content type to application/json, so that the client can understand it’s a particular payload type and compute it correctly.

All good but in case you haven’t realised, but when we sent the request to the service it was sent as HTTP GET; If we send a HTTP DELETE request or HTTP PUT, we’ll have the same json hello world response. You can test that yourself by running:

curl -X DELETE localhost:9000

Output:

{"message": "hello world"}

We need a type of selection control mechanism to control the flow of the program execution. In the example below we use a switch statement, but we could use if/else statements too.

func (h *Hello) ServeHTTP(rw http.ResponseWriter, r *http.Request) {
	h.l.Println("Hello world!")

	rw.Header().Set("Content-Type", "application/json")

	switch r.Method {
	case "GET":
		rw.WriteHeader(http.StatusOK)
		rw.Write([]byte(`{"message": "This is a HTTP GET response!"}`))
	case "POST":
		rw.WriteHeader(http.StatusCreated)
		rw.Write([]byte(`{"message": "This is a HTTP POST response!"}`))
	case "PUT":
		rw.WriteHeader(http.StatusAccepted)
		rw.Write([]byte(`{"message": "This is a HTTP PUT response1"}`))
	case "DELETE":
		rw.WriteHeader(http.StatusOK)
		rw.Write([]byte(`{"message": "This is a HTTP DELETE response!"}`))
	default:
		rw.WriteHeader(http.StatusNotFound)
		rw.Write([]byte(`{"message": "Not found!"}`))
	}
}

If we stop the golang program (our HTTP server) with CTRL+C, re-run it and execute a curl with -X POST or any other switch statement match we get a different json response.

{"message": "This is a HTTP POST response!"}

This far we’ve used net/http built in methods, but its quite limited. If you want to specify RESTful resources with proper HTTP methods, it is hard to work with the standard http.ServeMux. Also, for pretty URL paths (not fixed URL), that use variables, you’ll need to implement a custom mux (multiplexer). There are plenty of mux packages available but for the example bellow we’ll use Gorilla Mux - A powerful HTTP router and URL matcher for building Go web servers.

Add the package

go get -u github.com/gorilla/mux

And modify the code to include it (you’ll notice that its compatible with our net/http). To keep it short I’ve omited some code.

import (
  ...
	"github.com/gorilla/mux"
)

...

func main() {
	...

	hh := handlers.NewHello(l)

	sm := mux.NewRouter()

	sm.Handle("/", hh)

	...
}

If you re-run the server and do a GET request with curl you should see the same results as before.

We can now be a bit more specific regarding the HTTP method and assign specific function to each. We also need to use the HandleFunc, that has a different signature.

In the hello.go:

package handlers

import (
	"log"
	"net/http"
)

type Hello struct {
	l *log.Logger
}

func NewHello(l *log.Logger) *Hello {
	return &Hello{l}
}

func (h *Hello) Get(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusOK)
	rw.Write([]byte(`{"message": "This is a HTTP GET response!"}`))
}

func (h *Hello) Post(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusCreated)
	rw.Write([]byte(`{"message": "This is a HTTP POST response!"}`))
}

func (h *Hello) Put(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusAccepted)
	rw.Write([]byte(`{"message": "This is a HTTP PUT response1"}`))
}

func (h *Hello) Delete(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusOK)
	rw.Write([]byte(`{"message": "This is a HTTP DELETE response!"}`))
}

func (h *Hello) NotFound(rw http.ResponseWriter, r *http.Request) {
	rw.Header().Set("Content-Type", "application/json")
	rw.WriteHeader(http.StatusNotFound)
	rw.Write([]byte(`{"message": "Not found!"}`))
}

And finally the main.go that now has a handler and respective HTTP method assigned to a path.

package main

import (
	"context"
	"log"
	"net/http"
	"os"
	"os/signal"
	"time"

	"example.com/go-intro-microservices-pt2/handlers"
	"github.com/gorilla/mux"
)

func main() {
	l := log.New(os.Stdout, "rest-api", log.LstdFlags)

	hh := handlers.NewHello(l)

	sm := mux.NewRouter()

	sm.HandleFunc("/", hh.Get).Methods(http.MethodGet)
	sm.HandleFunc("/", hh.Post).Methods(http.MethodPost)
	sm.HandleFunc("/", hh.Put).Methods(http.MethodPut)
	sm.HandleFunc("/", hh.Delete).Methods(http.MethodDelete)
	sm.HandleFunc("/", hh.NotFound)

	s := &http.Server{
		Addr:         ":9000",
		Handler:      sm,
		IdleTimeout:  120 * time.Second,
		ReadTimeout:  1 * time.Second,
		WriteTimeout: 1 * time.Second,
	}

	go func() {
		err := s.ListenAndServe()
		if err != nil {
			l.Fatal(err)
		}
	}()

	sigChan := make(chan os.Signal)
	signal.Notify(sigChan, os.Interrupt)
	signal.Notify(sigChan, os.Kill)

	sig := <-sigChan
	l.Println("Terminate received, gracefully shuttingdown...", sig)

	tc, cancel := context.WithTimeout(context.Background(), 30*time.Second)
	defer cancel()

	s.Shutdown(tc)
}

At this point you must agree that the code is cleaner and easier to read.

If you re-run the server program and do a GET, POST, PUT, etc request you should see the correct response.

Subrouters are also supported and quite easy to do. You should spend some time checking the Gorilla Mux documentation to get to know a bit more.

func main() {
		...

		sm := mux.NewRouter()
		
		api := sm.PathPrefix("/api/v1").Subrouter()
		api.HandleFunc("/", hh.Get).Methods(http.MethodGet)
		api.HandleFunc("/", hh.Post).Methods(http.MethodPost)
		api.HandleFunc("/", hh.Put).Methods(http.MethodPut)
		api.HandleFunc("/", hh.Delete).Methods(http.MethodDelete)
		api.HandleFunc("/", hh.NotFound)

		...
}

You’d make requests to the same endpoint prefixed with /api/v1, which would result in the following, for example:

curl -v -X POST localhost:9000/api/v1/

Notice the /? A bit weird right? You can omit the / by modifying the path in your HandleFunc definition as follows:

func main() {
		...

		sm := mux.NewRouter()
		
		api := sm.PathPrefix("/api/v1").Subrouter()
		api.HandleFunc("", hh.Get).Methods(http.MethodGet)
		api.HandleFunc("", hh.Post).Methods(http.MethodPost)
		api.HandleFunc("", hh.Put).Methods(http.MethodPut)
		api.HandleFunc("", hh.Delete).Methods(http.MethodDelete)
		api.HandleFunc("", hh.NotFound)

		...
}

With the pattern above we could have a /api/v2 at some point in the future, while keeping the v1 alive.

We can also handle path and query parameters, please check the documentation in the Gorilla Mux repository .

A very basic example of that, picked from the Gorilla Mux repository is:

r.HandleFunc("/articles/{category}/", ArticlesCategoryHandler)

...

func ArticlesCategoryHandler(w http.ResponseWriter, r *http.Request) {
    vars := mux.Vars(r)
    w.WriteHeader(http.StatusOK)
    fmt.Fprintf(w, "Category: %v\n", vars["category"])
}

And this is all you need to know about the basic usage. For more advanced options you’ll have to check the documentation.

In the next part, we’ll find out how to Serialize data with encoding/json.

comments powered by Disqus