So I was playing with some new age hipster web tech stuff at work, namely angular material 10, as well as grpcweb (grpc for da browser), and was thinking about the foundations we’d build the next API and UI with.
There are a few “leading” standards for the web nowadays:
Formerly known as Swagger specs.
The spec is a large (imho incredibly) verbose json (or yaml) file (see this), which effectively declares the rest endpoints, the schema for request/response with the types.
List of languages for generatic client/server components can be seen here: https://github.com/OpenAPITools/openapi-generator
This is purely rest, and atleast in Go (unlike Kotlin used at work) there is no trivial way to generate the spec from code of the API.
There are a few attempts which either require you to write additional code that acts as the declaration or extensive error-prone comment abuse:
The nice part about OpenAPI specs is that you can slap a nice swagger ui on top of it which makes it easy to interact with the API, example:
Not so useful in our case, as we have UI, but incredibly useful for headless services which just expose APIs.
I guess in our scenario, we’d have to generate the server code from the spec, as otherwise I think it’s unlikely we’d be able to keep it in sync.
- Well supported for many languages
- Good ol’ orthodox REST
- Verbose and IMHO painful to write specs, atleast compared to GRPC.
- Only unary calls. No streaming support (looking at you events and all the things we use to poll)
This is what I believe Google are using for exposing their own services as rest services
Effectively it’s a set of annotations that you instrument your proto contract with (see this, which explains grpc-gateway how to turn the GRPC contract into a REST contract. The gateway can be mounted on the same mux as the GRPC service, so it’s not a separate component you run, just a library you have to use.
There is then a further plugin that allows you to generate an OpenAPI spec from the GRPC contract, which then allows you to generate typed clients for calling the gateway which then translates the calls to GRPC. This is obviously a generated contract and you probably would not want to modify that by hand.
You can/would use this OpenAPI spec to generate a strongly typed web client that can talk to your service via the gateway
- We gain GRPC support for non-web cases
- Good ol’ orthodox REST for web cases
- Well supported for many languages due to OpenAPI export
- More sensible schema to write
- REST stuff exposed by gateway only supports unary calls.
- Still have to write annotations on standard GRPC to make it work, yet again making it a bit verbose.
This is still “rest” behind the scenes, but there are a ton of weird headers flying around returning GRPC status codes and what not, so I don’t expect it to be inter-operable outside of it’s own ecosystem.
This does not require any extra annotations, and is able to generate strongly typed typescript (or js if you are a masochist) clients that make working on web stuff not miserable.
- GRPC support for non-web cases
- Support for service to client streaming calls in the browser (hello events)
- Trivial concise to write specs.
- Bye bye rest
- Only support languages that support GRPC going forward (which is probably most languages we care about, as there exists grpcurl which allows calling GRPC from the shell)
- Reverse proxy would require HTTP2 for real GRPC (HTTP1 for grpcweb)
I have an example, where I got grpc-web and grpc-gateway (hence transitively openapi specs) to work together, meaning we can have all of the following:
- Contract that declares the API (with annotations on how to map it to rest)
- Go code for the contract generated using Gogoproto
- A typescript client generated using grpcweb
- OpenAPI spec generated from the proto contract (can be used to generate rest clients)
- A single web server that serves all of this:
- Serve GRPC
- Serve GRPC gateway (convert rest traffic to GRPC traffic)
- Serve GRPC web server
- Serve OpenAPI spec on /swagger.json
- Serve Swagger UI on /swagger-ui/
But this feels like an overkill, and I still have to use magical proto annotations to explain it how to map GPRC calls to rest calls.
My gut says that GRPC + grpcweb should be enough. We would use grpcweb for our own UI use-case. If others want to talk to our API, they would use GRPC or our grpcweb client if they are in the browser.
This is a bit of a middle-finger to people who run reverse proxies from 1980s, that don’t support HTTP2, but I don’t expect these sort of users want to poke at vanilla GRPC at the GRPC API anyway, and reverse proxying grpcweb is still ok.
This would also be an opportunity for the “let’s have the config as a proto contract so users can easily use it language of their choice” I suggested a while ago.
What do you guys think?