9 min read

Building REST APIs with Golang: A Hands-On Guide (Part I)

Building REST APIs with Golang: A Hands-On Guide (Part I)
Photo by Sam Moghadam Khamseh / Unsplash

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 Namespaces 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.