Automagic Go <-> Typescript interface
Simplified communication between frontend and backend. All the power with none of the grudge
I have a system in place where, on the server side, I can define Go functions in the form of:
func Something(ctx *Context, input InputType) (OutputType, error) {
// ...
}
The system will generate Typescript bindings to the function so that, on the client side, I can just write:
let response = await server.Something(input)
This will call the function and return something akin to a `Result` type that contains either the data or the error message.
The “binding” code also generates the type definitions needed to make the code work: structs generate interfaces, and enum constants generate corresponding constants in the Typescript code.
The types have the exact same names, and the fields are also named exactly the same thing: no transformation to camelCase. Since the types are defined in Go, everything is CapitalCase.1
Here’s an actual example of server side code and the corresponding auto-generated client side code. It’s a picture I posted on Twitter.
The system exposes the functions at /rpc/FunctionName
and expects a POST request where the input is a JSON encoding of the InputType
and the response is a JSON encoding of the OutputType
.
That’s literally all it does. Here’s a picture from the network tab in the Chrome dev console:
Forget about ‘REST’
REST is one of those useless terms that the industry at large is crazy about for no particularly good reason.
Here’s why you should absolutely forget about it:
REST is merely a post-hoc description of how the web worked when it was mostly just documents, forms, “GET” and “POST” requests.2
When doing asynchronous data requests form Javascript to your Servers, REST has absolutely no place.
One of the most common bullshit discussions people have around their “REST”ful APIs is whether this or that endpoint should respond to the “POST” vs the “PUT” http method.
In the HTTP specs, these verbs are supposed to have different semantics. But the cruicial point is that these semantics mostly describe how the browser is supposed to interact with web servers. The browser will refrain from submitting a form twice if it has the POST method. This is why sometimes you get a bewildering alert message when you try to refresh a page:
If you got to your current page by clicking a button on a form, and the form had the “POST” method, then the browser will not let you refresh the page without this warning. Why? Because HTTP says that “POST” is supposed to create a new resource. Since you got this page by sending a POST request, refreshing the page entails sending the POST request again.
This is simply NOT APPLICABLE to when you are implementing an RPC on top of HTTP with async requests.
How to generate the binding code
I don’t care what language you are working with on the server side: there is a way to auto generate code. If your language does not have introspection capabilities, then you can use a parser to get information about your functions.
In my Go code, I use a combination of both: the language has reflection capabilities that allows me to extract enough information from my procedures to generate interfaces for its structs, but no reflection about enum constants what-so-ever, so for that I use parsing: I parse the Go files that define the enum constants and extract the enum info that way.
Now, having a complete and total solution that can bind to *any* code is going to be a daunting task and give you very little return on your investment.
Instead, you should make some deliberate choices to simplify the problem.
In my case, I made the following choices:
Functions only take one input type and one output type
Errors are reported to the client side as strings
No transformation of type and field names. UpperCase is fine.
A ‘Context’ input carries minimal information about the request: mostly the user’s auth token. The idea is to make the functions unaware of being called over “HTTP”; they could just as well be called from the command line.
Custom types that JSONify to another type must be annotated as such.
Generally speaking, the code I’m working on is not published on any public repo, but I’ve put the bulk of the code I wrote for this system in a github gist:
https://gist.github.com/hasenj/95dbc2321dc584093537b30289bc5a58
You can’t use this a package you depend on via a url. The idea is that you download it (copy it locally) to your codebase and make changes to it as necessary to make it fit your use case.
I added a small README with how to use it. It only generates the type declaration, not the bindings for the functions, but if you read it and understand it, you should be able to write that code yourself.
Happy Coding!
There’s no reason for Tyepscript code to enforce a specific casing convention. In the case of Go however, we don’t have much of a choice.