Building REST APIs with Golang: A Hands-On Guide (Part I)
Introduction
Golang is a popular choice for building web applications and APIs due to its simplicity, fast compilation times, and excellent support for concurrency. In this guide, we will cover the fundamental concepts and best practices for building REST APIs with Golang, as well as provide step-by-step instructions for creating a fully functional API.
Whether you are a beginner looking to get started with Golang or an experienced developer looking to improve your skills, this guide is for you. So let's get started building some awesome REST APIs with Golang!
Nowadays, it is essential for a business to design it's web software as a modular system. On a high architectural level, the modularity between represantational layer and data and business logic layer is usually achieved by using an HTTP API.
These days, there are couple of ways to serve an API over HTTP, like GraphQL or JSON APIs, but nevertheless, the most used are REST APIs. It's popular, because it's been around for a while, and even if GraphQL solves some of the problems that REST APIs standard does have, it is still heavily used.
Go ecosystem, provides a lot of tools to create a REST API, even HTTP package, allows to create HTTP server, for handling HTTP requests, but it lacks lot of features for a modern REST server, that should be created from scratch. For that, there also a lot of battle tested frameworks, that comprise a lot of tools to fix that issue. The most popular is Gorilla Mux and Gin framework.
In this tutorial, I will prefer to use Gin, as from my standpoint, it has a more pleasant and simple API; it quite similar to Gorilla, but with additional stuff, like request body validation and deserialization out of the box, which is very handy.
Disclaimer: This series of tutorials will explore the tooling for creating a REST API only. It is not a practical example, which will use entire suite of external tooling, like data storages, or third party APIs. For those purposes there will be additional tutorials
Principles of REST APIs
In order to achieve a REpresantational State Transfer standard, there are several principles used as guideline in order to design a REST API:
Client-Server - I this makes quite of sense. The communication should directed from client to server, and server should process request and send back a response. That is the lifecycle of communication round for a REST API
Uniform Interface - this means that URI should identify a resource, and give access to manipulate the state of that resource. Also, it should respond with self-descriptive message.
Stateless - this means that no information should be stored on application layer, whatsoever. This is responsibility of data layer, which is represented by data storage systems, like databases, filesystems, cloud storage providers, etc.
Cacheable - I think the name speaks by itself, but I will repeat, that this means that when a resource is accessed once, it should be stored on a faster read layer, for faster serving, and not accessing the entire infrastructure for repeating request.
Layered - this means that components within an application should have a single responsibility on how to process data, and handle data based on it's purpose.
Having that in mind, we can proceed to create the boilerplate for our REST API.
Setting all up
We will start by initializing a Go project, using this command:
$ mkdir rest-boilerplate
$ go mod init github.com/<your-github-name>/<name-of-project>
Now let's add the first library to our project, Gin framework
$ go get github.com/gin-gonic/gin
Next, let's structure our app. I will use the following schema:
/
cmd/
api/
app.go
main.go
pkg/
models/
middlewares/
handlers/
services/
...
tests/
...
This is corresponding to Go standard package, as it sets all the package used by app in pkg
directory. There is also tests
directory, which in our case will store integration tests. The cmd
will store app entrypoints packages, which will leverage the pkg
internal packages.
Let's start to define our HTTP server, which will be a rough implementation, which we will refactor in the future sections:
var port *string
func init() {
port = flag.String("port", "8080", "Port on which server will listen for requests")
}
func main() {
flag.Parse()
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "Ping: OK")
})
server := &http.Server{
Addr: fmt.Sprintf(":%s", *port)
Handler: router
}
err := server.ListenAndServe()
if err != nil && errors.Is(err, http.ErrServerClosed) {
log.Printf("[server error]: %s\n", err)
}
}
So, let's take this example apart: first, before running the main
function, we need to make sure about the port, that will expose our HTTP server. I think that the best way is to define it as an argument for out build, and have a default value, which in our case is port 8080
.
Next, when entering main
function, we parse flags, and we are creating a root router instance. This means that we will add handlers for root namespace of our API, however Gin allows to use nested namespaces, around a specific resourse.
For now, we will create a single endpoint handler, which is just a healthcheck, making sure that we have access to our server.
Next, we create a http.Server
instance, which allows a better control for how we shutdown application, when we have multiple threads within our process. For now we will use this server
just to ListenAndServe
, and handle the error if there are any on server runtime.
So let's fire up this one:
$ go run cmd/api/main.go
And send a request using curl
:
$ curl http://localhost:8080/ping
Great, our server is running.
Now, let's add a gracefull shutdown. This is usefull as all the resources allocated for our server will be successfully cleaned up by Go's garbage collector. The most common issue are hanging goroutines.
This how it will look in the code:
var port *string
func init() {
port = flag.String("port", "8080", "Port on which server will listen for requests")
}
func main() {
flag.Parse()
router := gin.Default()
router.GET("/ping", func(c *gin.Context) {
c.String(http.StatusOK, "Ping: OK")
})
server := &http.Server{
Addr: fmt.Sprintf(":%s", *port)
Handler: router
}
go func() {
err := server.ListenAndServe()
if err != nil && errors.Is(err, http.ErrServerClosed) {
log.Printf("[server error]: %s\n", err)
}
}()
shutdown := make(chan os.Signal)
defer close(shutdown)
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)
<-shutdown
ctx, cancel := context.WithTimeout(context.Background(), 5 * time.Second)
defer cancel()
err := server.Shutdown(ctx)
if err != nil {
log.Fatalf("[server-force-shutdown]:\n\tError %q", err.Error())
}
log.Println("[server-exit]: OK")
}
In this example, we moved the ListenAndServer
handling into a separate gorouting, so that it would not interfere with main
goroutine. At the same time, main
goroutine creates a channel of type os.Signal
, which is basically a message sent to our program process by the OS, known as signals.
Notify
function, will make sure to write that one of signal types from argument list is being received, and in that case it will pass a message to channel, that a SIGINT
(program interrupt signal) or SIGTERM
(program termination signal) is received; otherwise main
goroutine execution is blocked.
If a signal is received, execution is unblocked, and we Shutdown
our server execution, with a timeout context. The good thing of using timeout context, is that in our case, if the server won't shut down in 5 seconds, the context will return an error, and then the server will be forced to closed, by invoking log.Fatalf
.
Choosing DI container
In the previous section, we saw how we can define a simple HTTP server implementation, how to map a simple handler function, and to close gracefully a server.
But having a modular architecture, can allow us replace pieces of our code implementations, without affecting other parts. This is also valid for our application structure.
This way, we need to refactor our application, so that we will store logic related to app initialization based on configuration from a single component. Also, this will allow us to have an API with components that can be replaced.
So let's define one:
type App struct {
server *http.Handler
listener net.Listener
}
func NewApp(listener net.Listener, handler http.Handler) *App {
return &App{
listener: listener,
server: &http.Server{
Handler: handler,
},
}
}
func (a *App) Run() error {
return a.server.Serve(a.listener)
}
func (a *App) CloseWithContext(ctx context.Context) error {
return a.server.Shutdown(ctx)
}
func (a *App) Close() error {
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
defer cancel()
return a.CloseWithContext(ctx)
}
Ok, now that we created the App, we will need net.Listener
and http.Handler
in order to inject them in newly created app structure. Notice, both dependencies are defined as interface types, so the could be replaced, with anything that implement that interfaces.
Also, I have moved all the close server logic into the App
struct, as it should be responsible for shutting down the server.
Next we will create the Gin router initialisation function, that will basically return an http.Handler
compatible instance:
type Namespace struct {
Path string
Middlewares []gin.HandlerFunc
Routes []Route
}
type Route struct {
Path string
Method string
Handler gin.HandlerFunc
}
func NewRouter(groups ...Namespace) http.Handler {
router := gin.Default()
for _, group := range groups {
newGroup := router.Group(group.Path)
for _, middleware := range group.Middlewares {
newGroup.Use(middleware)
}
for _, route := range group.Routes {
newGroup.Handle(route.Method, route.Path, route.Handler)
}
}
return router
}
This router function will take a bunch of namespaces as parameters, and turn into groups, so now we prepared everything we need to run our app.
The main
function will now look like this:
var port *string
func init() {
port = flag.String("port", "8080", "Port on which server will listen for requests")
}
func main() {
flag.Parse()
listener, err := net.Listen("tcp", fmt.Sprintf(":%s", *port))
if err != nil {
log.Fatalf("[error][listener-init]: %q", err)
}
middleware := func(c *gin.Context) {
log.Printf("[request][received]: %q", c.Request.URL.Path)
}
pingNamespace := Namespace{
Path: "/",
Middlewares: []gin.HandlerFunc{middleware},
Routes: []Route{
{
Path: "/ping",
Method: http.MethodGet,
Handler: func(ctx *gin.Context) {
ctx.String(http.StatusOK, "Ping: OK")
},
},
},
}
router := NewRouter(pingNamespace)
app := NewApp(listener, router)
go func() {
err = app.Run()
if err != nil {
log.Printf("[server error]: %s\n", err)
}
}()
shutdown := make(chan os.Signal, 1)
defer close(shutdown)
signal.Notify(shutdown, syscall.SIGINT, syscall.SIGTERM)
<-shutdown
err = app.Close()
if err != nil {
log.Fatalf("[server-force-shutdown]:\n\tError %q", err.Error())
}
log.Println("[server-exit]: OK")
}
This looks pretty much the same as it was previously, the changes being the pingNamespace
and middleware
function added, as well as all new functions that we created in order to instantiate the router and the app. The app
instance now handles the server initialisation and shutdown.
In order to clean this app a bit, we should create a function that will instantiate a new App
, and will deal with all dependencies:
func pingNamespace() Namespace {
return Namespace{
Path: "/",
Middlewares: []gin.HandlerFunc{func(c *gin.Context) {
log.Printf("[request][received]: %q", c.Request.URL.Path)
}},
Routes: []Route{
{
Path: "/ping",
Method: http.MethodGet,
Handler: func(ctx *gin.Context) {
ctx.String(http.StatusOK, "Ping: OK")
},
},
},
}
}
type AppBuilderFunc func() (*App, error)
func BuildAppManually() (*App, error) {
listener, err := net.Listen("tcp", fmt.Sprintf(":%s", *port))
if err != nil {
log.Printf("[error][listener-init]: %q\n", err)
return nil, err
}
router := NewRouter(pingNamespace())
return NewApp(listener, router), nil
}
So we basically created a new function type, that will handle all dependencies, and return an app instance, with all dependencies.
The main problem of the manual DI container, is that for a large scale applications, the tree of dependencies will grow exponentially, and it is a pain to handle that amount of dependencies.
In order to solve that problem, a DI (dependency injection) container should be used. There are numbers of DI container libraries in the Go ecosystem, but the most common used are Uber's Dig, and Google's Wire.
The difference between the two is that Dig uses the reflection in runtime in order to achieve a flexible way of managing dependencies; compared to Google's Wire, which is generating the code for dependencies management, which I consider most useful, as generated code could be reused, and no runtime overhead is required.
For that, we need first of all to install the Wire library:
$ go install github.com/google/wire/cmd/wire@latest
If there are some issue when installing just with this command, please consult the official repository, for documentation and issue raise.
After install, I created a file in cmd/api
package, called wire.go
, and I added the following content:
// +build wireinject
package main
import (
"github.com/CristianCurteanu/go-rest-api/pkg/handlers/ping"
"github.com/CristianCurteanu/go-rest-api/pkg/routers"
"github.com/google/wire"
)
func routes() []routers.Namespace {
return []routers.Namespace{
ping.PingNamespace(),
}
}
func BuildAppDepCompile(port string) (*App, error) {
wire.Build(
NewListener,
routes,
routers.NewRouter,
NewApp,
)
return &App{}, nil
}
The wire
will automatically take care of code generation, but the only important thing to keep in mind is the wire.Build
function. It is the way how wire
"understands" how to build the dependency graph, based on argument dependencies types, as well as type bindings. For instance, I had some trouble with varchar Namespace
s for NewRouter function. Eventually, I used a separate function, that puts together all the namespaces.
After running generate command:
$ go run github.com/google/wire/cmd/wire ./cmd/api
there was generated an App initialization function:
// Code generated by Wire. DO NOT EDIT.
//go:generate go run github.com/google/wire/cmd/wire
//go:build !wireinject
// +build !wireinject
package main
import (
"github.com/CristianCurteanu/go-rest-api/pkg/handlers/ping"
"github.com/CristianCurteanu/go-rest-api/pkg/routers"
)
// Injectors from wire.go:
func BuildAppDepCompile(port2 string) (*App, error) {
listener, err := NewListener(port2)
if err != nil {
return nil, err
}
v := routes()
handler := routers.NewRouter(v...)
app := NewApp(listener, handler)
return app, nil
}
// wire.go:
func routes() []routers.Namespace {
return []routers.Namespace{ping.PingNamespace()}
}
This is actually very handy, as it has the same signature as manually defined App instantiation function, but now, handling the dependencies will be much easier, and we will see it in future articles.
Now we can try to build and run it:
$ go build -o ~/server ./cmd/api
$ cd && ./server -port=3051
This should spin up the server on port 3051
, and in order to test it, we can run following command:
$ curl http://localhost:3051/ping
Ping: OK%
which means that server is running properly.
Wrapping up
In this article we built the skeleton of a Golang web service using Gin framework, in order to enhance a modular structure. The solution is still raw, and in future articles we will take a look how it can be improved, and will do some refactoring.
In the next article we will take a deeper look on handlers and requests processing, using Gin.
Member discussion