Build A Go API
Everything you need to do before writing any endpoints
A few months ago, we wanted to create a microservice that would handle sending and receiving text messages and storing the messages in a MongoDB cluster. There are a lot of articles on why you might choose Go for your next API, and a lot of examples of what that API might look like. This article is neither of those. Instead, I will talk about five things you need to think about after you decide to create a Go API but before you write any business logic.
1. Learn the Go way
2. Create a package structure
Depending on the scope of your project, this can range from being one of the most important decisions to the least important. If you’re building a small service that you don’t anticipate growing much in the near future, you can probably get away with having everything in a single package. Otherwise, the right package structure can be the difference between an elegant codebase and a behemoth that nobody enjoys working on. When picking a package structure, our goal was for the codebase to be:
- Intuitive: a developer should be able to guess, simply by looking at the folders structure, where any specific logic exists
- Simple: a developer should be able to understand a given package in isolation, without having to explore other packages
- Maintainable: for common workflows (e.g. add a new endpoint), a developer should be changing a minimal number of packages
Based on these goals, this is the package structure we landed on. It’s not a forever solution; it will definitely evolve as we continue to build the service. But for now, it works pretty well for us:
|-- api // contains all of the handler logic
|-- types // defines endpoint request and response types
|-- config // server configuration
|-- db // database connection logic
|-- models // database object structs, getters and setters
|-- vendor // external packages
If you are planning on having multiple applications in a single project, want to use your main application as a library, or want to run your application as a binary, a common Go convention is to create a
cmd folder containing subdirectories for each application, and you can put your
main.go files inside their application subdirectories.
3. Choose (or don’t) a framework
Go’s built-in net/http package, which provides HTTP server implementations, is known for being very fully featured. A lot of advice we came across was to stick to the built-in package for various reasons: it works well, you limit external dependencies, and you can better learn the language by sticking to core packages. Using this package would be a very valid choice.
However, with this being our first Go service, having limited Go experience on the team, and with limited resources, we decided to explore some of the available frameworks. Our main goal was simplicity, and we eventually settled on chi for its lightweight implementation and its middleware library. There are a lot of other good options and a lot of opinions online; it is easy to get bogged down in making this decision. My advice is to just pick one that seems good enough, and implement it in a way that is replaceable.
In the interest of the same goals we had when choosing a package structure (intuitive, simple, and maintainable code), we wanted our endpoints to simply take in a request argument and return a response body and error, and we wanted to declare the routes for a given resource within the api package where we implement those routes. To accomplish this, we defined a
Route struct that has a method, path, and handler function, and created a route wrapper that converted our handlers into the format expected by chi:
This way, we can define our routes in the api package as:
With this implementation, a developer writing an endpoint never needs to know what framework we used and can focus on the business logic. And finally, we can define our middleware and mount all of our routes on a single chi router:
4. Figure out package management
This used to be a slightly more complicated process — we used dep to add packages to a vendor folder — but Go 1.11 introduced modules as a standard feature and Go 1.13 will finalize the feature, so use that. If you’ve already started using something else, here’s a helpful quick guide to migrate to go mod.
5. Go fmt and test your code
Formatting your code using gofmt means that your code looks like almost all other Go code out there. You may have stylistic preferences that differ from gofmt, but ignore them —they don’t really matter. The advantage of having a single code style is that once you get used to parsing your Go code, you will be quicker at parsing all Go code.
In order to not rely on developers gofmting their code changes, we added pre-commit hooks that do it for us. We chose to use goimports instead of gofmt — it does the same thing as gofmt but also cleans up missing and unreferenced imports (win!).
We also use go test to test our code and set up a pre-push hook to make sure that only code that passes our tests is checked in to the shared repo. There is a lot that can be said about how to properly write tests in Go, which I won’t get into here, but Test with Go is a good resource to get you started.
Optional: Dockerize it
Docker containers are everywhere these days, and you probably already know about their portability, reliability, and lightweight nature. If you are using docker containers for your deployments, you can use the standard Go base image to build your container.
One thing you might notice though is how large the container can get. In our case, a basic API with just 5 endpoints was about 800MB large! This is because the container requires Go installed (obviously), which requires Go’s dependencies, which includes pretty much an entire operating system.
Instead, what we can do is use go build to compile our code into a binary, and then mount the binary into an empty container. In our case, doing this reduced our container size by 100x to just 8MB!
Bonus: Use gin to live reload your server
Like most developers, I like to automate things that are annoying or repetitive or, worse, both. So while I am writing code, instead of using
go run to run the server locally and test my changes, I use gin, which allows me to write code with the server always running in the background. When I make changes, I only have to save the code and the changes automagically propagate to the running server. No more
ctrl+c — up arrow — enter!
There are a lot of early decisions you make when starting a new project, and these decisions can heavily impact the project’s future. Some of them are easily reversible, and some can back you into a corner with no way out. It’s important to take a moment and think through these decisions, and I hope this was a helpful list of things to think about as you start building your own Go API. If you have other considerations that you think are important, or have alternate suggestions for some of our decisions, do let us know — we’re constantly looking to learn and improve our work!