Writing RESTful APIs in Go, 3 years later

I’ve started working with Go in early 2017, and since then most of my work has been writing RESTful APIs with it. With time I gained experience. and I constantly change the way I write APIs in Go. After a year of working with Go, I’ve released Gorsk — a Golang RESTful starter kit, and an update to it 6 months later. I get many emails and questions on how to use it properly, which means that something like Gorsk is highly needed. Over time I’d like to keep it up-to-date with my latest views on how to write REST APIs in Go, and this blog post serves as a first step in the next iteration.

Nowadays one of the most popular ways to interact with a server is via Web services that conform to the REST architectural style, called RESTful Web services. Most of the programming languages support HTTP calls out of the box, which makes integration with REST services a breeze.

With Go, you can write and organize your code in many different ways, there is no right or wrong. What matters the most is the code’s maintainability. It may be opinionated, but opinions backed by experience are way more valuable than those without.

Over the years I’ve worked with various clients that needed a REST API developed.

I’ve seen projects that used multiple ORM at the same time. Projects using git branches for differentiating user features based on tier. Projects connecting and disconnecting to a SQL database multiple times per HTTP request.

For most of the clients, I’ve implemented a project similar to Gorsk, with some changes I made over time. I’ll reflect on these changes in this blog post.

Initializing repository

An important notice is that some tooling support is not yet there for projects outside of GoPath. (VSCode being the most prominent).

Project structure / package layout

For a single domain, like users, I put all the code in user directory inside the root. The transport code is located in transport/http.go. If there are any other transports besides HTTP, like AMQP, RPC or SOAP it belongs here.

Code for interacting with other services, like database, email, 3rd party APIs is placed under /client/service. In Gorsk client is named platform. I’ve seen that Uber uses gateway instead of client, so you can name it whatever works for you.

Dependencies and interfaces for the application service are placed inside user/service.go, and the actual implementation is in user/user.go If the application service does more things than simple CRUD operations, I split the user.go into multiple files or even packages.

As for configuration, I keep using simple YAML files for nearly three years, and it serves me quite well. There are plenty of packages out there like Viper that support far more features than a single YAML file, but I never needed them. I store all the secrets as environment variables and manage them either manually or by a CI/CD tool.

Dependencies

For the HTTP router, I’ve settled with chi. It uses Go’s standard HTTP handlers (w http.ResponseWriter, r *http.Request), unlike Echo and Gin. It comes with highly needed middleware packages (like logger, recoverer, requestID) and the provided render package, used for marshaling and unmarshaling HTTP requests works quite well.

Unless I’m using some platform-specific databases, like DynamoDB and Datastore, go-pg is my go-to library for database access. I’ve tried using database/sql and sqlx extensively, but go-pg’s feature-set and author’s support is second to none.

Logging and metrics

I log only requests that resulted in an unexpected error (unless the customer/client wants to log everything). All the expected outcomes, those return HTTP statuses between 200 and 500, are to be exported to a monitoring system like Prometheus, and later displayed in the dashboard such as Grafana. Good metrics provide excellent observability and indicate the usage patterns of your system.

My current solution for logging is wrapping it in middleware function, and writing to log if the HTTP status code is 500 or above.

Errors are logged in stdout and read by a service like GrayLog or LogStash from there.

As for errors, I use a small custom package to return HTTP status codes with error messages. It utilizes chi’s render package by implementing the Render interface. Also by implementing the Error interface, the Response type can be used where error is expected.

If an unexpected error happens, I wrap it using fmt.Errorf("something happened: %w", err), put it log and return 500 to the customer. All errors that are expected to happen return HTTP status codes in range 400-410, and are not being logged.

Serverless