The Best Go API Framework for RESTful services

2 Apr 2024

The built in http server library in Golang is an ‘S’ tier masterpiece that works fantastically well, even for large monolithic applications. But it’s not quite right. What I miss is something that allows me to abstract away some of the http details and just focus on a simple service definition. The HTTP API it generates should be idiomatic REST, but the implementation should be largely focused around simple type interfaces. This dream framework should give me an implementation experience that looks similar to GRPC, but exposes a REST api instead of protobuf.

Interested in my vision for this? Read On!

It should generate the best documentation

The best developer experience is always underpinned by fantastic documentation. OpenAPI is a good start. Being explorable, and being able to try out the API is key. The Documentation should be easy to extend and seamlessly update based on your type definitions and code changes.

Beyond just documentation being generated, Open API is the right choice because it's the lingua-franca of the API world. Tools like Postman make it easy to test and import API’s. Smart API gateways plug on to an open api spec with additional functionality like authentication, authorization, load balancing, rate limiting and caching.

It should be REST-ish

Sorry we won’t be diving into the details of the REST warfare of the early noughties today. But this perfect API framework for go should make it dead simple to make a json based API, that fully supports the HTTP platform it builds on. We want to be as standard as possible so that caches can respect e-tags and we can use PUT/POST/DELETE/GET to their full extent.

The grpc gateway project is an example of what not to do. It provides a very simple reverse proxy for your GRPC service that also generates an Open API schema for your service. The problem is that it railroads you into a POST call for every RPC. Thankfully there are escape hatches, but it makes things unnecessarily complex. Our dream API framework for go should simply support making an api that can leverage all the good parts of the http platform.

Let the developer focus on the business logic

Our days are already filled with writing and managing config in a number of horrible languages (HCL, yaml, json, ini etc). The dream framework keeps this config to an absolute minimum.

I frame this as 'complexity points'. We only have so much capacity for fitting new tools and lanugages in to our head. We should be optimising on this to provide the most value possible. Is memorizing yet another config schema really the way? Why not leverage what we already know well, the type system of the language. This is why I think the best solution is to eschew config in favour of using what the language provides us.

To make it painfully clear. Let me spend my complexity points on solving the problem, doing the valuable thing, not memorizing another DSL for your framework. This means not writing the schema first in protobuf, or open api YAML (or something else).

A better way in my opinion is to build your framework on top of the go languages simple and easy to use type system. Let’s just keep it simple and build it on top of interfaces, structs and error types.

Provide simple escape hatches when needed

Leaky abstractions don’t always suck. They are actually essential for web frameworks. Our dream API framework should allow you to drop back to the raw material we are working on. You should always be able to turn a request handler into a simple http.HandlerFunc. Too often, frameworks will pile so much abstraction that it makes it really hard to find your way to the familiar. This is a common problem that is not often thought about when evaluating what tools to use. What this often looks like is happy sailing, churning out features until you inevitably get stuck and need to fork your way to a solution. You then get the joy of supporting an old fork, or merging in all the upstream changes. This is yet another unnecessary spend of the 'complexity points' that your team can handle.

Escape hatches can also mean being able to exit the framework. For our fictional case here, that would mean falling back to the excellent built in http server library provided by golang. Need to ditch the framework and go old_school? Not a problem!

What I looked at, what sucks and what is almost OK

There is no API web framework for golang that is Quite right. They either go too far in my opinion building all these abstractions that hide the fact you are working in the web platform, or they simply don’t go far enough. When it comes to the higher level frameworks, it’s sometimes hard to even know you are working on the web, and there’s layers and layers of abstractions between your handlers and the actual requests/responses. Then other minimalist libraries can be a bit like the wild west. They invite you to shoot yourself in the foot or require you to introduce reams of boilerplate. This opens up opprotunities for inconsistent code. To work effectively in the minimalist framework a team needs huge amounts of discipline.

Let's look at two fo the best candidates I found in a recent review of golang web frameworks that focus on open api definitions.

Go swagger

https://goswagger.io/ is a promising candidate, in that you can do all the rest-ish features you need. But they went too far with the abstraction in my opinion. It generates a huge amount of unneccessary code, and the request handler interface is fundamentally flawed. Less is more, and this framework proves it. For example, consider the addition of jsonschema and full openapi spec to the code generation in goswagger. I admit, it’s a cool feat of engineering. However, the complexity of the types generated being forced into the golang language is not worth the cost.

Another example of this negative impact feature is the interface chosen for request handlers. The way you can escape to a raw http middleware is great, but the problem is that you lose all the type safety support from the framework. My thesis is that it is a net positive if you catch MORE errors at compile time. The interface type chosen here is an example of the opposite, you can easily write a handler that compiles, but returns the wrong data type!

I also despise needing to start with writing json schema and YAML first 😿

Swag-go

https://github.com/swaggo/swag also has some nice things to be said about it. It is a simpler and less abstract option. You can easily mix/match and plug just about any web framework into it, and that’s very cool, but it misses some features I would love to have around isolating the business logic. For one, it doesn’t expose an interface that allows your code to not compile if the schema is incorrect. This is thanks to its ingenious design of code generating an open api schema based on the code comment. It’s just a shame that this doesn’t hook in to forcing some type safety on the request handler.

GRPC-gateway

https://github.com/grpc-ecosystem/grpc-gateway If you are a GRPC enjoyer, this might be the choice for you. It wraps a service definition, generating an http 1.1 server and open api spec. You run a tiny little reverse proxy that forwards requests to your backing grpc service. It’s quite easy to wrap your own http middleware and just write your business logic as a simple GRPC service.

Did you think I was going to only say nice things about this project? Sorry, I love it, but I can’t not poke a few holes.

First, it requires a side-car to deploy. This is probably adding a significant amount of drain on your non value generating faffery quota.

There is also a quality issue with the generated openapi schema. All rpc calls will turn in to POST requests. This is not ideal if you are planning on supporting a big distributed caching front end. It's a problem that can be solved via supported annotations in the schema, but again -- you're swimming against the current here.

What does better look like?

My vision for the best framework to write an API in golang is a little different to what exists at the moment. We take inspiration and build on the great work done prior. Overall, the framework should look pretty familiar. It is about sane defaults, and a massive respect for developer productivity over unnecessary faffery.

The 'best framework' will never focus on forcing you in to any of the following:

  • Orms
  • Mapping to database schemas
  • Non JSON interfaces
  • HTML
  • Hypermedia
  • Novel types beyond the Golang standards.

Instead it will focus on:

  • Generating great documentation
  • Escape hatches for plain old http.Server interface
  • Your service interface defines the API schema
  • A simple subset of open API 2.0

Behold -- my tiny Dumb Version half page diagram

Let's dive in to the above 5 minutes exalidraw-job. The request mapper, response mapper is provided automatically and generated on your behalf.

This includes routing, and choosing the right method to call. In this example service.GetArticles . All of the code you work on for implementing your new handler is encapsulated in your simple handler function

func (s *Service) GetArticle(ctx context.Context, articleID int) (Article, error)

The framework is clever enough to figure out the route, the response type, the request parameter is articleID, and the response type is shaped like an Article. If an error is returned, it should handle that and set the appropriate response code depending on the type of error.

If you need more granular control on your request/response mapper, just copy over the generated function to your own code package. The code generation mechanism is the smart part, it can scan your own package to see if you are implementing your own direct handler for the path. I.E if you have a `func (s *Service) GetArticle(r *http.Request, w http.ResponseWriter) in your own code, it will not generate the handler for you, allowing you to take control of the request/response mapping.

You can start with the sane defaults, and if eventually you need fine grained control -- it's all their ready for you to take over.

A Golang API Framework That Respects You the developer

This framework would be one that puts the developer experience first. Think Rails, but with more explainable and obvious magic. We want the simplicity of the go language but the full strength of HTTP as an engine of application state. The framework would be as simple as defining a service interface, but let you out-grow the framework and support a code base for 10+ years.

The core pillars

  • Simplicity: Built on top of interfaces, structs, and error types, making it easy to understand and use.
  • Flexibility: Escape hatches to raw HTTP handlers, allowing developers to drop back to the underlying Go HTTP library when needed.
  • Type safety: Catch as many errors at compile time as possible by enforcing return types from request handlers.
  • Developer Friendly: Generates great documentation. Doesn’t force you to write schema first in painful config languages.
  • Lightweight: The framework is lightweight, with no external dependencies, making it easy to deploy and maintain.

If you are interested in collaborating on this project, or you can point me to one that satisfies what I’m looking for ( even in other languages ), let me know!

Subscribe to my Newsletter

Want to know more? Put your email in the box for updates on my blog posts and projects. Emails sent a maximum of twice a month.