My Beautiful, Ugly Stack
A journey through several heavens and hells.
At my company, the fast majority of our applications run a React frontend (pure JavaScript) driven by a Java REST API. And this is… fine. In some ways it’s very lowest-common-denominator development, but there’s a lot of usefulness in that. All those popular tools tend to be relatively well-supported and documented. In fact, it’s in the places where we’ve tried to go a little fresher, a little more hip, that things haven’t always held up so well.
Hell One
Our APIs are powered by (a bespoke wrapper around) a Java framework with a very concise syntax that we’ve all sort of grown to appreciate:
GET("/records/{id}", purchaseRecordRoute);
But that framework has also seen almost a complete halt in development for the past several years. (No shade on the dev, either. If it’s not a living, it’s not a living!)
Now, if you can believe it, our custom little wrapper:
- Is overly intertwined with other systems we use
- Was built by a guy who is no longer with the company
Raise your hand if you guessed either.
This lovely little combination has made it rather difficult to test out alternate stacks. It’s difficult to justify building out a decent chunk of our auth system again just because you want to use Spring or whatever.
Hell Two
JavaScript.
Hell Thre-
Okay hold on.
Hell Two
JavaScript. Love it or hate it, it’s not going away any time soon. And there’s a great degree to which I love JavaScript. As far as pure syntax, I think it’s pretty damn hard to beat. Building up objects, spread operators, first-class functions, etc. are all really fun to play with. There are a million gotchas and edge cases, but when I was still more in the picking-it-up stage, I would often take a barely-educated guess at some syntax and have it run perfectly. There’s something fairly intuitive about it in that way, at least to me.
However,
I have been bitten by the types bug. If I’m working on a large, or just unfamiliar project, and I have to guess or go digging for type information, I am not going to be a happy camper.
console.log('wtf is this', wtf)
I know you’ve written this exact code before, and I am so sorry.
Keeping track of all this crap is a pain, and it really shouldn’t have to be. God forbid someone change something in the backend, too. Changing or removing fields unchecked is a great way to break frontends, and adding new fields without meticulous documentation is a great way to never have your fields used (or worse, to have somebody waste time building out a different way to pull them).
Heaven One
Typescript: It’s pretty nice. It lives somewhere between
- a beautifully flexible and comprehensive type system, and
- a tiny barnacle clinging on for dear life to an enormous typeless whale
I really enjoy using it, most of the time. Sometimes getting the types to line up how I need is a big pain in the rear, but when they’re in there, it I can practically feel the footguns’ safety clicking on.
SubHell A
Microsoft. I do not like them. Their grubby hands were all over the internet once and by jove they do not need that kind of power again.
If they ever use this immense leverage to (further) enshittify the web again, I will find my home amongst the bog people, where I truly belong.
Heaven Two
OpenAPI
Purgatory?
So, in my attempt to turn my hells into heavens, I’ve extended my company’s framework wrapper to support THIS:
public record RecordListResponse(List<PurchaseRecords> purchaseRecords) {}
public record RecordListRequest(@FromPath UUID id) {}
// ...
get("/records/{id}", RecordListRequest.class, RecordListResponse.class,
new Metadata()
.summary("Retrieve purchase records for the given account")
.tags(SwaggerTags.RECORDS.toString()),
purchaseRecordRoute);
“What, in the reflection-driven blazes,” you may find yourself asking, “is that?”
This is my attempt at bringing some of that OpenAPI joy into this mess we call our web framework.
Notable successes:
It actually works
This contraption manages to generate semi-sane OpenAPI specs for our use-case. Requests and
responses in JSON; @FromPath, @FromBody, etc. annotations work how you’d imagine they do, and
every project implicitly hosts a Swagger Web UI and spec file generated from its endpoints.
I’ll admit that the first iteration of this process played it relatively fast and loose with the types. It was difficult to enforce at compile time that data needed to be specified before it could be requested. But, after a few passes, it maintains a goood amount of strictness throughout, with a feeeeww begrudging escape hatches for when an endpoint needs to do something really weird.
It’s very well-tested
Knowing that this project was so central to so many of our products, and knowing that those early iterations were not likely to hold, I wrote a comprehensive test suite. Now, I’ll be the first to concede that library-type code is probably easier to write tests for than business logic, but this sucker is sturdy. Those tests have caught subtle breaking changes a number of times, and I’d have been afraid of iterating too quickly without them.
Notable flaws:
Ugly syntax
I will not elaborate.
Also built by just one bozo
When I am hit by a truck (or inevitably become too absorbed in bog culture to continue my employment) I can only provide well-wishes for the civilized folks that I once knew. The code is organized for what it is, but what it is is a hacky kludge injected into a (bespoke wrapper around a) framework that was simply not built for it.
But even with that, there may still be hope.
Heaven… Three???
Ugly as it is, this new OpenAPI data opens up several interesting possibilities.
For one thing, generated TypeScript clients. I’ll be the first to tell you that we aren’t using this to its full capabilities, but we’ve started using openapi-generator-cli to build out components that our frontends can rely on. (N.b. this is a third-party tool that I believe wraps around the first-party swagger-codegen.) We’ve yet to start using the generated HTTP functions, but even the data models alone are incredibly nice to have. We can regenerate those models at build time, right from the live swagger in the backend, and if any fields have changed or no longer exist, it’s an error! A beautiful, beautiful, compile-time error.
This, in itself, is very fun to me. But it’s also sort of the snake eating itself – in a good way! That is to say, it’s also possible to generate server-side code using Swagger. Theoretically, possibly, maybe, there’s a world where we don’t need to build out our backends in a different, better-supported framework. We might be able to “just” generate a new one.
Even then, obviously most of the underlying business/db/auth logic would need to be moved over manually, but hey, having the endpoints in place would be a heck of a start.