Setting up a Go API Server

I'm building the reread.to service, and writing these articles to explain the decisions I'm making and how I'm building it. Now it's time to set up my API server. As mentioned before, the API server will be written in Go, and the frontend app in Typescript (using Next.js).

I like shiny new things so of course I tried GraphQL when it was hot, and I loved it, and it was the best. Until it wasn’t. And it was cumbersome to work with at times. And it took a week to set up. And in the end, I didn’t need most of the flexibility it gave me. So, now until I have an API that is streaming terabytes of data, I’ll use crusty old REST which matches my crusty old developer bones. Don’t get me wrong though, I still like the concept of GraphQL, but for all the reason above I didn't opt for it for reread.

Using the same principle of keeping things simple and efficient, I chose Echo as the web framework. It doesn’t try to do too much, it’s well documented, and looks pretty clean. I don't have a favorite framework in Go yet, because I've never built a server in Go before. So, I looked around, and Echo seemed the most appealing to me. I don’t have a detailed or very deeply thought through explanation for this so if you like a different framework for your Go server needs, that’s cool. Let me know what and why in the comments.

First things first, be warned: I don't have much experience with Go. Expect me to do things the wrong way. I learn best by stumbling, so I hope this series will at least be entertaining, if not instructional. That said, I do have ample experience with building and running services, so I would occasionally know what I’m doing, and maybe that’s useful for someone.

Go project setup

Since I'm just starting out, setting up a simple go project is a single command:

go mod init reread.to/server

This is within the /server folder of my repo. For the basic folder setup, refer to the 'setting up the development environment' article in the series. Now that we have a Go project, I'll start by creating a few packages for the various components of my API server.

Here is a current snapshot:

./server
├── auth/
├── clients/
├── config/
├── responsetypes/
├── routes/
├── server/
├── go.mod
├── go.sum
├── go.sum
└── main.go

Each folder is a Go package with the same name as the folder. I'll give a brief overview of what each folder does:

  • auth/: Package to help with authentication
  • clients/: Clients will contain code for things like the DB client, and any other services reread will need to connect to
  • config/: This is a simple package to read environment variables, config files and provide a central Config struct that will be passed around to configure various parts of the server
  • responsetypes/: The package containing all the response types for our API so that Echo knows that to marshal to JSON
  • routes/: The package that contains all the routing setup for the server. I'll get into the details of the routes package more soon as it's an important package.
  • server/: Everything related to setting up the actual server. Middlewares, loggers, etc. will be contained in this package.

That's the gist of the architecture I have right now. I'm sure I'm missing a lot here, some of it is on purpose because I haven't thought about how to set it up and others I'll find out as I progress with server development.

Testing

I am what some would call a 'fair weather TDD fan'. TDD stands for 'Test driven development'. I like to do TDD, but when it's a pain to set up or I don't know how to best test something, I just ignore the existence of tests.

What I've found to help with this condition to have a test setup from the start. Less friction to write tests will ensure that I have more tests despite my laziness.

Since reread has zero customers, and will probably have one for the immediate future, I'm not too worried about test coverage, but I would like to know when I push code that the server isn't crashing.

So for now, I start with writing a simple test that ensures the server is creatable:

func Test_CreateServer(t *testing.T) {
    assert := assert.New(t)

    e := server.Create(&config.Config{
        IsTest:             true,
    })

    assert.NotNil(e)
}

and a test for a health endpoint:

func Test_ServerHealthEndpoint(t *testing.T) {
    assert := assert.New(t)
    e := echo.New()
    req := httptest.NewRequest(http.MethodGet, "/_health", strings.NewReader(""))
    rec := httptest.NewRecorder()
    c := e.NewContext(req, rec)

    assert.NoError(routes.HealthEndpoint(c))
    assert.Equal(http.StatusOK, rec.Code)
}

The goal between the two tests is to be able to verify that the current code can create a server and be able to receive a request.

The Test_ServerHealthEndpoint test above might be a bit confusing so I'll explain it a bit more:

  • Create a new request test object without a body for the /_health endpoint
  • Create an object that records the response
  • Build an Echo Context object using the above request and response
  • Call an endpoint method directly, instead of passing through the echo server
  • Check if the endpoint works and is sending back a 200 status

The main function

The server set up so far is very simple, the main file looks like so:

func main() {
    config, err := config.LoadConfig(".", "app.env")
    log := server.GetLogger()

    if err != nil {
        log.Fatal("unable to load config: ", err)
    }

    e := server.Create(&config)

     e.Logger.Fatal(e.Start(fmt.Sprintf(":%s", config.Port)))
}

There are the following things happening here:

  • Load the config
  • Create the server object
  • Start listening on the configured port

I prefer simple main functions because they tend to be a magnet for all kinds of things as the project proceeds. I've seen thousands of lines of code in main files too many times, and everyone is fearful to touch it. So now, I try to refactor all the work out of the main function. Here, I've taken out the server setup, the logger initialization, and the configuration loading. Once I have the DB set up, it too will hopefully take up a couple of lines within the main function.

Because I've taken the bulk of the code out of the main function, it's not very instructive, so let's dive into the constituent parts. In this article I'll only elaborate on the actual server setup, but will keep the logging and configuration setup for another article. I'll be happier to share them once I have a production deployment working.

Server setup

Most server frameworks I've used have broadly two components:

  • Middlewares, and
  • Routes

The two parts differ in terms of their usage. Routes are the main functions of a server. They are called by clients, have a name (path) and expect a response. Conversely, middleware are not known to clients, are called by the server while processing requests, and provide facilities common between routes.

My current server setup looks like so:

func Create(config *config.Config) *echo.Echo {
    e := echo.New()

    addMiddlewares(e, config)
    routes.AddRoutes(e, config)

    return e
}

Simply, it creates the echo server object, adds all middleware, sets up routes, and returns the echo server object. That's then used by the main function to start listening. This kind of setup also makes it easy for me to test the server as seen in the test code earlier.

I'm not using any middleware yet, so that's not really useful to see. The main function AddRoutes within the routes package looks like so:

func AddRoutes(e *echo.Echo, config *config.Config) {
    e.GET("/_health", HealthEndpoint)
}

Each time I want to add a route, I'll add an *Endpoint function to the package and then wire it up to a path and and REST method in this function. Since we only have a health endpoint right now, here's how it looks:

func HealthEndpoint(c echo.Context) error {
    return c.JSON(http.StatusOK, responsetypes.Health{Ok: true, Message: "Ok"})
}

One thing to note is that I could've called the function healthEndpoint, since both the AddRoutes and healthEndpoint are in the same package routes. However, that will make me unable to test the endpoint separately. I like to add the _test suffix to the package for tests, so making the endpoints start with a capital letter is the only way to expose them outside the routes package.

Another new part above is the responsetypes.Health struct which has the following definition:

type Health struct {
    Message string
    Ok      bool
}

With these pieces, we now have a go server, which listens on a port and responds with JSON when you hit the /_health endpoint.

Next up, I'll continue to set up the Go server. Upcoming is dockerizing the server, making the docker container autoreload on code changes, adding graceful shutdown, and more.