I love designing extension APIs
At a previous job where I did 50/50 front-end/back-end work, we had to be HIPAA compliant. We also needed to support “offline mode” in the browser. This involved downloading data during sign in, fetching encryption keys from the backend, encrypting the data before writing it to browser storage, decrypting the data on-demand, and ensuring the encryption keys weren’t readily accessible after a session timed out. (This was years before the Web Crypto interface was available.)
There were also WebSocket connections to establish and Angular components in the application shell to conditionally show or hide.
All of this activity was triggered by changes in session state: session start, session refresh, session timeout, session end, etc. Our code was littered with session callbacks like:
SessionService.onStart(function (sess) => {
// do stuff
});
This mostly worked, but we soon began noticing unpredictable behavior. The order of some of these callbacks became important, but that order wasn’t explicitly defined anywhere. Debugging was a nightmare. More and more features began registering these callbacks, compounding the issues. We needed finer and finer grained session states to register callbacks for.
Session Extensions
I decided we needed a single place in the code to register “extensions” to session behavior. This removed ambiguity about the order of events.
SessionService.extensions([
SessionTimerService,
EncryptionKeyService,
PrecachingService,
WebSocketService,
...
]);
By using a well-defined API, our extensions could integrate deeply with our rather complex HIPAA-compliant session system. Some of the API hooks were purely reactive, some could transform session data, and they were all Promise-based, allowing some hooks to be “blocking” and others to run in parallel.
class EncryptionKeyService {
onSessionStart(sess) {
// download encryption keys
}
onSessionClear() {
// clear keys from memory
}
}
It worked well and we all lived happily ever after. But I got a new job, and I missed working on my little extension system.
Blueprinter
After beginning work at Procore I was quickly introduced to Procore’s open source JSON serializer, Blueprinter. Never having been a fan of Active Model Serializers, I was a fast convert.
Having a deep interest in ActiveRecord N+1 query problems and eager loading (see occams-record and uberloader), I saw some opportunities in Blueprinter. After getting a formal reflection API merged, I began work on a very, very small extension API. blueprinter-activerecord was the result. It uses Blueprinter’s and ActiveRecord’s reflection APIs to identify and preload ActiveRecord associations during a blueprint render.
Unfortunately, Blueprinter’s architecture precluded the kind of extension API I really wanted to write. While blueprinter-activerecord worked well, there were edge cases and bugs that couldn’t be fully addressed. And there were community members with cool ideas for other extensions that simply couldn’t be supported.
Before I joined Blueprinter as a maintainer there had been talk about what a V2 might look like. There were lots of ideas, and I added “first-class extensions” to the list. V2 is still a work in progress, but extensions sit at the center of the design. Many core features (conditional fields, JSON library support, and others) are implemented using the public extension API.
Middleware based extensions
My early designs were “hook” like: Blueprinter is doing event X, so run all X hooks. Later, someone brought up the idea of “Russian doll caching” (like in Rails views, but for blueprints). Inspired by Rack, I pivoted to a middleware-based architecture.
A simple caching extension might look like:
class MyCacheExtension < Blueprinter::Extension
def around_blueprint(context)
key = cache_key context
Rails.cache(key, expires_in: 10.minutes) do
yield context
end
end
end
The middleware approach allowed me to combine many of the “regular” hooks into fewer, middleware-style ones, while offering a more powerful API. My hope is that many of the feature requests we get (that we sometimes must unfortunately turn down) can be implemented using V2’s extension system.
Blueprinter V2 is still a work in progress, and the extension API will probably be tweaked further before release. But it’s the coolest extension API I’ve worked on so far, and I’m excited to see what people do with it.