A sample API implementation in Go

Go is a good choice for creating highly optimised, lean software. Full-stack frameworks don't fit well into this philosophy. It doesn't mean that there aren't any full-featured frameworks in Go. However, very often there's no need for one.

For the sake of building a basic API project I decided to use go-chi, a light-weight router as well as go-pg, a Golang ORM with focus on PostgreSQL as the main building blocks.

https://github.com/antennaio/go-notes

Features

  • modular code organization
  • examples of standard CRUD operations
  • environment dependent configuration (.env file)
  • authentication using JSON Web Tokens
  • request validation
  • query and request logs
  • route/struct binding using middleware
  • autogenerated slugs using BeforeInsert/BeforeUpdate hooks
  • soft deletes
  • migrations
  • integration tests
  • modd - recompiles and runs the api package in response to filesystem changes
  • Docker support

Application structure

There are 3 entry points to the application:

  • The API server (can be started using go run api/*.go command)
  • Migrations runner (can be started using go run migrate/*.go command)
  • Unit/intergrated tests (can be started using go test ./... command)

The application is initialized using a single Initialize method, which allows to easily spin it up in different contexts.

func main() {
    a := app.App{}
    a.Initialize(
        env.Getenv("POSTGRES_DB_NAME"),
        env.Getenv("POSTGRES_DB_USER"),
        ...
    )

    port := env.GetenvWithFallback("PORT", ":8080")

    a.Serve(port)
}

The API functionality is split into multiple modules:

go-notes/api/auth
go-notes/api/note
go-notes/api/user
...

Each module contains a set of middleware, route definitions, route handlers, etc.

Integrated tests

There's good infrastructure to write unit tests in Go. Each file can have an accompanying *_test.go file that contains tests. In case of an API project it's really helpful to have integrated tests as well. The standard tooling doesn't offer much in this area. However, if we consider what integrated tests actually are it's likely there's no special tooling necessary. 🤔

A typical integrated test can be broken down into four steps:

  • setup (the database is migrated to a certain state)
  • request (a request is made)
  • assertions (the database can be inspected at that stage)
  • teardown (the database is reverted to the base state)

The standard Go library lets us construct a new request using http.NewRequest() and record it using httptest.NewRecorder() method.

The initial database setup and teardown can be accomplished using database migrations and SQL queries.

This seems to work reasonably well for the purpose of writing integration tests.

Take a look here for an example.

Docker support

Docker support was added using recipes from the Building Docker Containers for Go Applications article, which I recommend checking out.

There are a few real-world problems the article doesn't cover though. For this particular project I decided to build two different images for production and development environments:

  • The development image contains modd, which recompiles the program in response to filesystem changes. It maps the source to the filesystem on the host OS using a volume.
  • The production image on the other hand contains compiled binaries and no source code at all. It's created using a multistage build and is based on alpine linux distribution, which makes the resulting image very small.

From my experience it's also necessary to create an entry point (a shell script) that waits until the database server is ready before starting the application (more info here).