When building Microservices the de facto way to do it is to use both HTTP, as the protocol, and REST, as the way to represent resources. HTTP is literally the foundation of the World Wide Web and it is supported by multiple programmings in one way or another, so using HTTP is a no brainer because it could be used by any Frontend and Backend code with few to no external dependencies.
REST is an acronym for REpresentational State Transfer coined by Roy Fielding and introduced in his doctoral dissertation, it is an architectural style that defines guidelines when building web services, it's not a standard but it does use standards like HTTP (and its verbs to represent actions) and payload/message formats (like JSON or XML), to mention a few.
This is the first of a series of posts covering steps for building REST APIs in Go.
Disclaimer: This post includes Amazon affiliate links. If you click on one of them and you make a purchase I'll earn a commission. Please notice your final price is not affected at all by using those links.
Introduction to HTTP Handlers
Go includes all the necessary building blocks for implementing Webservices in its standard library, including things like HTTP Servers, different multiplexers and way to define HTTP handlers, as well as way to render content in different encodings like JSON or XML, something like the following works for creating an HTTP server:
package main
import (
"fmt"
"net/http"
)
func main() {
http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "Path: %s!", r.URL.Path[1:])
})
http.ListenAndServe(":8080", nil)
}
Accessing it using curl
:
❯ curl -X GET http://localhost:8080/resources
Path: resources!
Using the standard library to define our HTTP Resources definitely works, however one of the biggest difficulties when using the standard library is the need to add extra code to build handlers that happen to have the same base path or require variables as pattern matching.
For example, assuming we need to define resources for /resources
, /resources/{id}
and /resources/{id}/values
using the standard library will require us to define code to identify /resources/{id}
and /resources/{id}/values
after handling /resources
.
This is better explained when requesting something like this:
❯ curl -X GET http://localhost:8080/resources/1234/values
Path: resources/1234/values!
In this case our implemented handler needs to determine those arguments from the path itself.
For cases like this I recommend using a third party package called github.com/gorilla/mux
.
Using github.com/gorilla/mux
github.com/gorilla/mux
is a powerful URL router and dispatcher, it lets us implement handlers meant to be matched by different options, including paths, schemes or query values, to mention a few. To solve the problem we had before with the handler requiring us to define code to determine the other resources we could write something like:
package main
import (
"fmt"
"log"
"net/http"
"time"
"github.com/gorilla/mux"
)
func main() {
router := mux.NewRouter()
router.HandleFunc("/resources", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/resources")
})
router.HandleFunc("/resources/{id:[0-9]+}", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/resources/{id:[0-9]+}: %s!", mux.Vars(r)["id"])
})
router.HandleFunc("/resources/{id:[0-9]+}/values", func(w http.ResponseWriter, r *http.Request) {
fmt.Fprintf(w, "/resources/{id:[0-9]+}/values: %s", mux.Vars(r)["id"])
})
srv := &http.Server{
Handler: router,
Addr: ":8080",
WriteTimeout: 15 * time.Second,
ReadTimeout: 15 * time.Second,
}
log.Fatal(srv.ListenAndServe())
}
Which then we can request using curl
like we did before but this time the three different resources and clearly handled independently:
❯ curl http://localhost:8080/resources
/resources
❯ curl http://localhost:8080/resources/123
/resources/{id:[0-9]+}: 123!
❯ curl http://localhost:8080/resources/456/values
/resources/{id:[0-9]+}/values: 456
Implementing resources
The code used for this post is available on Github.
I previously mentioned Hexagonal Architecture during the Domain Driven Design and Project Layout post, using that as base we can define a new type TaskHandler
that defines all the different nouns meant to represent our resources:
func (t *TaskHandler) Register(r *mux.Router) {
r.HandleFunc("/tasks", t.create).Methods(http.MethodPost)
r.HandleFunc(fmt.Sprintf("/tasks/{taskId:%s}", uuidRegEx), t.task).Methods(http.MethodGet)
r.HandleFunc(fmt.Sprintf("/tasks/{taskId:%s}", uuidRegEx), t.update).Methods(http.MethodPut)
}
The naming of those resources usually is defined as collections, in our case /tasks
, and depending on the needs we could define subcollections to represent collections associated to an entity belonging to our parent collection, so an example of this would be a hypothetical resource called /tasks/{taskId}/categories
.
In the context of our "To Do Microservice" domain we are defining three methods that map exactly to three different HTTP verbs and three different actions defined in our TaskService
Application Service, those HTTP verbs usually map to CRUD operations:
- POST -> Create
- GET -> Read
- PUT -> Update
- DELETE -> Delete
Therefore our type defined above is mapped like:
-
POST /tasks
->(*TaskHandler).create
used to create tasks, -
GET /tasks/{id}
->(*TaskHandler).task
used to retrieve tasks by id, and -
PUT /tasks/{id}
->(*TaskHandler).update
used to update tasks by id - We didn't implement
DELETE /tasks/{id}
this time, but in a future commit we will see it appear.
Implementing the required resources also requires us to define concrete types for handling requests and responses, the way I like to recommend implementing those is to define types with the following naming <MethodName><ResourceName><Request|Response>
, for example the type representing the payload to create a Task
via create
would be:
type CreateTasksRequest struct {
Description string `json:"description"`
}
And the response type response from task
would be:
type GetTasksResponse struct {
Task Task `json:"task"`
}
Although a bit repetitive defining concrete types for requests and responses allows us to define more rules to those values when defining our OpenAPI document, which will be discussed in future posts.
Parting words
With this post we kick off the series describing the steps for building REST APIs, building HTTP-based webservices is relatively simple when using the standard library, however things could become difficult when trying to deal with nested or parametrized resources, in those cases using a third party package like github.com/gorilla/mux
makes sense.
Because we are using Domain Driven Design together with the Hexagonal Architecture we can explicitly indicate types representing the resources we are trying to model, mapping HTTP verbs to concrete methods and defining specific types for requests and responses lets us pave the
way for changes we will need to add documentation via OpenAPI 3.
Recommended Reading
If you're looking to sink your teeth into more REST and Web Programming I recommend the following books:
Top comments (1)
thansk for sharing!