DEV Community

Alireza Bashiri
Alireza Bashiri

Posted on • Updated on

Building a finance tracking REST API using Go with TDD - Part 1

Building a simple REST API using Go always could be challenging from designing the architecture to implement it 'cause there's no standard way to do it and developers (software architectures) are free to do whatever they want which comes with a cost.

After almost 6 years of building services and REST APIs using RubyOnRails, I realized that developers have a passion for abstraction but that's not always a good choice. For example; If you look at almost every RubyOnRails projects there's too much abstraction and implicit things going on which makes the learning curve far more of a curve, starting pretty shallow.

Today I'm going to show you a different perspective of designing REST APIs with Go which like a good parent instead of allowing us to do anything teaches to practice, patience, and perseverance.

Let's get started, at first every Go project must be created in $GOPATH. My $GOPATH is at, ~/go/src/ ~/go/ . But we're going to create the project under ~/go/src/github.com/budget-api be able to download the project using go get.

So we're going to run these commands, in CLI.

cd ~/go/src/github.com/azbshiri
mkdir budget-api
touch main.go
Enter fullscreen mode Exit fullscreen mode

(If you don't have github.com/username, you would create it using mkdir -p github.com/username after changing the directory to ~/go/src/)

Okay, we're going to use PostgreSQL as our database and Gorilla toolkit to help us build a RESTful API. For now, we just need github.com/go-pg/pg to be able to connect to PostgreSQL and github.com/gorilla/mux for routing.

go get github.com/go-pg/pg
go get github.com/gorilla/mux
Enter fullscreen mode Exit fullscreen mode

Alright now that we have the prerequisites, we should think how we're going to use them and design the architecture.

I'm going to use a struct as a container for controller/handler actions called server to be able to access database, router and etc. So I create a new file called server.go and put the declaration in it.

// server.go
package main

type server struct {
    db *pg.DB
    mux *mux.Router
}
Enter fullscreen mode Exit fullscreen mode

And create a factory function to initialize a new server (that would help in testing which I'll show in the future)

// server.go
package main


type server struct {
    db *pg.DB
    mux *mux.Router
}

func newServer(db *pg.DB, mux *mux.Router) *server {
    s := server{db, mux}
    s.routes() // register handlers
    return &s
}
Enter fullscreen mode Exit fullscreen mode

We need a routes.go to declare paths for APIs

// routes.go
func (s *server) routes() {
    // register handlers here
}
Enter fullscreen mode Exit fullscreen mode

Now we have a file hierarchy like the below

.
├── main.go
├── rotues.go
└── server.go
Enter fullscreen mode Exit fullscreen mode

To be able to run the server we should implement http.Handler interface so we add a new pointer function to server called ServeHTTP and serve registered handlers in the router (mux)

// server.go
package main

import (
    "net/http"

    "github.com/go-pg/pg"
    "github.com/gorilla/mux"
)

type server struct {
    db     *pg.DB
    mux    *mux.Router
}

func newServer(db *pg.DB, mux *mux.Router) *server {
    server := server{db, mux}
    server.routes() // register handlers
    return &server
}

func (s *server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
    s.mux.ServeHTTP(w, r)
}
Enter fullscreen mode Exit fullscreen mode

Now we can update the main.go file to run the server

package main

import (
    "net/http"

    "github.com/go-pg/pg"
    "github.com/gorilla/mux"
)

func main() {
    db := pg.Connect(&pg.Options{
        User:     "alireza",
        Password: "alireza",
    })

    mux := mux.NewRouter()
    server := newServer(db, mux)
    http.ListenAndServe(":8080", server)
}
Enter fullscreen mode Exit fullscreen mode

At this point, we don't have any registered handlers which would lead to an API so we have just a useless running server but we have the standard, to begin with which is a great thing to speed up the development process.

Due to I'm a TDD guru, so let's just begin with a test to write our first API. As you know we're going to build a finance tracking REST API so we're going to have Budget business model and call the API endpoint /budgets.

// budgets_test.go
package main

import (
    "net/http"
    "os"
    "testing"

    "github.com/azbshiri/common/test"
    "github.com/go-pg/pg"
    "github.com/gorilla/mux"
    "github.com/stretchr/testify/assert"
)

var testServer *server

func TestMain(m *testing.M) {
    testServer = newServer(
        pg.Connect(&pg.Options{
            User:     "alireza",
            Password: "alireza",
            Database: "alireza",
        }),
        mux.NewRouter(),
    )
    os.Exit(m.Run())
}

func TestGetBudgets(t *testing.T) {
    res, err := test.DoRequest(testServer, "GET", "/budgets", nil)
    assert.NoError(t, err)
    assert.Equal(t, res.Code, http.StatusOK)
}
Enter fullscreen mode Exit fullscreen mode

To write the above test we used two libraries github.com/stretchr/testify/assert and github.com/azbshiri/common/test the first one is for making assertion easier and the second one is a little bit complicated which I built myself to reduce duplication of initializing of a http.Request and a httptest.ResponseRecorder and pass them to the existing router to be able to capture the response (That's normal way of writing integration tests in Go for further information you can check out https://golang.org/src/net/http/httptest/example_test.go).

Run the test

go test -v .
=== RUN   TestGetBudgets
-------- FAIL: TestGetBudgets (0.00s)
        budgets_test.go:42:
                        Error Trace:    budgets_test.go:42
                        Error:          Not equal:
                                        expected: 404
                                        actual  : 200
                        Test:           TestGetBudgets
FAIL
exit status 1
FAIL    github.com/azbshiri/budget-api  0.018s

shell returned 1
Enter fullscreen mode Exit fullscreen mode

Phew! The plan was to cover everything in a single post but honestly this is getting longer than anticipated so kindly permit me to break this into a series of posts.

Once again, thanks for reading and don’t forget to leave your questions and/or suggestions as comments below.

Top comments (4)

Collapse
 
gaumala profile image
Gabriel Aumala

Minor observation: GOPATH is usually just ~/go and it contains more than src.

I tried to do something like this once, but Go's built-in test framework isn't really designed for this sort of thing. When running integration tests with the database you usually want to clean the database on setup/teardown, but Go runs each test file in parallel so there's the chance that you wipe the database in one file while other tests are running. It's not feasible to have all your tests in a single file, so I just abandoned the idea. I'm curious if you have find a workaround for this.

Also, if most of your work is done by a database (like the vast majority of REST APIs), then the best things about Go (concurrency & performance) can't really help you. I personally believe that Go isn't really a good choice for REST APIs. There are probably simpler alternatives. However it's probably a good exercise to write a small API in Go if your sole objective is to learn more about HTTP since it's very low level compared to more popular languages.

Collapse
 
alirezabashiri profile image
Alireza Bashiri • Edited

Good catch! $GOPATH I mean. That's obviously a mistake. About the test cases in a single file I have no problem with it, for example; if you look at Django testing that's how they're doing it. I believe in one simple package and many test files and I always do it.

Here's a simple example;

budgets.go
budgets_test.go
categories.go
categories_test.go
transactions.go
transactions_test.go

Also as I'm going to show, with github.com/go-pg/orm.CreateTableOptions which has a specific field called Temp we're going to create a temporary database for each test case and do the isolation.

func TestMain(m *testing.M) {
    testServer = newServer(
        pg.Connect(&pg.Options{
            User:     "alireza",
            Password: "alireza",
            Database: "alireza_test",
        }),
        mux.NewRouter(),
    )

    testServer.db.CreateTable(&budget{}, &orm.CreateTableOptions{
        Temp: true,
    })

    os.Exit(m.Run())
}

I've more than 5 years of experience developing various web applications using RubyOnRails and Django but I've to tell you there's too much complexity in them but they're productive for people who are master and I think one reason that Go got too much attention was that it's easy for even juniors to master and write a simple web application or a RESTful API. But simplicity comes at a cost and using a programming language and a specific framework needs experience and dedication and choosing Go could be a wrong decision in some cases.

Collapse
 
pmalhaire profile image
pmalhaire

Hi nice post, I guess there is a small typo :
├── main.go
├── rotues.go

I guess is should be routes.go

Collapse
 
jaysonesmith profile image
Jayson Smith

Nice work! I'm working on a Go API that deal with budget info, but a different purpose.