Building APIs with Go — Part 1 Choosing our router
This post is the start of a series to help you learn and make good decisions about your API in Go.
We’ve been testing different languages, libraries, and coding practices for our product at my current employer. After all of that, we’ve concluded that there’s hardly a language as good as Go to build backend APIs currently. This conclusion applies to our team; you should always consider your decisions with your team in mind.
With that in mind, let’s make it clear that the focus of this series is a good development experience. Still, we don’t want to add unnecessary overhead or avoid performance improvements, but that’s not coming at the cost of our productivity.
Let’s start with our first review.
Routing
The first thing that our API needs are routes, so we need to look at our router. For that, we have a lot of options, including the standard router, basic routers, and feature-rich frameworks.
Standard
It is okay if you don’t need to route by the request methods or use route params. If you need that, then you’d have to implement it manually. You can write some if statements inside the handler to check the method. For params, you’d need to use a regex or something similar. It’s good to keep this solution in mind, but it’s not what we want.
Chi (https://github.com/go-chi/chi)
Great router, with support for middlewares, params, methods, and sub routers / routing groups. The API is compatible with the standard library. The router itself is pretty lean since it focuses just on routing, nothing else.
Gin (https://github.com/gin-gonic/gin)
Gin wraps the request and the writer with its Context, providing useful functions for you. This way, you can access params and format responses a bit easier. Although it’s considered a framework, it doesn’t feel bloated.
Echo (https://github.com/labstack/echo)
Echo has a similar syntax to Gin. The difference here is that it takes care of more parts of your app, the logging and the error handling. You can customize the logger to use your library, but honestly, that’s not that easy since you have to write a bridge that supports the echo logger interface. We gave up on this framework because of that. We prefer to use something that focuses only on routing and is less opinionated about the rest of our app.
Fiber (https://github.com/gofiber/fiber)
Fiber has the best syntax out of the bunch. It takes care of the routing, providing many valuable middlewares with clear documentation and handling the error handling, similar to how echo does it. The difference is that this framework isn’t as bloated as echo and not as opinionated as well. It leaves a lot of room for customizations. The dealbreaker of Fiber is that it uses the fasthttp library instead of the standard net/http under the hood. Also, it isn’t easy to achieve proper context propagation with this. A few existing libraries assume that you are using net/http and others work with context propagation, so they are incompatible with Fiber.
Benchmarks
D3v2 instances have four vCPU and 14 GB ram. The green bar represents the number of requests that the server can handle per second.
Prefork is a technique that uses the native OS features to handle parallelism instead of the built-in go solution. You can read more about it here: 🤔 What is the purpose of prefork? · Issue #180 · gofiber/fiber (github.com)
On Fiber, you can enable it by just passing a flag to the server. However, on the other routers/frameworks, you have to depend on an external library or write a bit of code to accomplish this. Since Fiber focuses on high performance, you will typically find that optimizations like these are easier to do, and you’ll see more content about it in their community than the rest.
When looking at a graph like this, you may think that Fiber is excellent, outperforming Chi by leaps and bounds, and you should use it because it’s so much faster.
However, in this second benchmark, you can see that if you add a single query using the default SQL library, they are all pretty similar.
Unless you desperately need to worry about performance on your API instead of just scaling the container, it barely makes a difference, to be honest.
Conclusion
We’ve chosen to stick with the Chi router in our case. We can use any library for JSON serialization and customize everything without having to stick with the decisions made by a framework. Other good options were the Gin framework or Fiber.
We’re by no means declaring that Chi is the best out of here, just the best for us. It would be best if you kept in mind that each option has its focus areas and a community around them that will behave differently based on what they want to achieve with their package.
In part 2 of this series, we will start writing our API and go through some code design and folder structure decisions, we will be using the Chi router there, but you should be able to apply it to any of the options listed here.
Additional notes
We’ve considered many other frameworks and routers, including the httprouter, used by the Gin framework. However, these were the best ones in our opinion.
Another framework that seemed suitable for us was the Iris framework. Unfortunately, we ended up not using or referring to it here since it appears as this framework got banned from the awesome go list. A portion of the community also heavily despises the author.