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!
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.
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.
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.
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!
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.
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:
Instead it will focus on:
http.Server
interfaceBehold -- 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.
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
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!