Jacob O'Bryant | 20 Jun 2025
Merry solstice. After about a year, I'm roughly 80% done with the Yakread rewrite. Now all that's left is the remaining 80%. My last post is still a good explanation of the new Biff things I've been hacking on as part of that. Over the past couple weeks I've also been thinking about how to do forms.
So far Biff hasn't provided anything special for forms: if you need an email address, you do
[:input {:name "email"} ...]
, you get the value as a string from (-> request :params :email)
,
you parse it if needed (not needed in this case), then you stick it in a map like {:user/email email, ...}
or whatever. Works fine for small forms; no need to over-complicate things.
But what if you have form with 50 fields? It would be nice if we could get EDN from the frontend,
e.g. {:user/email "abc@example.com", :user/age 666}
instead of {:email "abc@example.com", :age "666"}
. Same as you get if you're doing a cljs frontend instead of htmx. htmx users deserve nice
things too!
I've started rendering my form fields like [:input {:name (pr-str :user/email)} ...]
(turns out
:name
will accept just about anything) and then using a
wrap-parse-form
middleware to parse the requests. That function attempts to parse each key in the form params with
edn/read-string
(fast-edn, actually), skipping keys that
fail. For each parsed key, we then check your Biff app's Malli schema to see if that key is defined
and what its type is. We use the type to figure out how to parse the form value. There are default
parse functions for a few common types (int
is Long/parseLong
, :uuid
is parse-uuid
, etc).
For other types, you can define a custom form parser in your schema, for example:
(def schema
{::cents [:int {:biff.form/parser
#(-> %
(Float/parseFloat)
(* 100)
(Math/Round))}]
:ad [:map {:closed true}
[:ad/budget ::cents]
...
Now if I have a form field like [:input {:name (pr-str :ad/budget)} ...]
and the user types in
12.34
, on the backend I'll get {:ad/budget 1234, ...}
automagically.
The form data isn't quite self-describing like EDN is: it relies on schema defined somewhere outside
the form. I started out doing stuff like [:input {:name (pr-str {:field :user/favorite-number, :type :int})} ...]
(seriously, you really can put anything in :name
), but since I'm writing this
middleware for Biff apps specifically, I didn't feel like that approach was adding much value. And
I'm all about value.
What about forms with multiple entities? If your :name
value is a vector like (pr-str [:user :user/email])
, then wrap-parse-form
will do an (assoc-in params [:user :user/email] ...)
. I
don't at the moment have any special support for arrays of things, but you can do :name (pr-str [:users 3 :user/email])
and then you'll get {:users {3 {:user/email ...}}}
in the request.
Other Biff news
Remaining things in the Yakread TODO list include finishing the ad system, adding premium plans, precomputing some recommendation models so that page loads are faster, and setting up email digests of your subscriptions. How long could that take? Surely not long! Oh, and then I just need to migrate all the users over from the currently-in-production Yakread as well as another similar app that stopped being profitable last year... but yes, certainly not long.
Once that's humming along and my monthly side project operational costs are back in the double digits, it'll be time for a much needed Biff release. I'll extract some of the stuff from Yakread and package it up real nice, and then go through some maintenance tasks that have been... festering, shall we say. And then it's time for...
xᴛᴅʙ ᴠᴇʀsɪᴏɴ 2: at last. Everyone's favorite 4-letter immutable database is out of beta. Which means it's really time to get Biff on it. I figure Yakread, once the rewrite is done, will make a nice open-source example of porting a nontrivial app from XTDB v1 to v2. So expect a big Biff release with migration guide and all that. Hopefully by the end of the year 😬. Maybe I could even look into integrating XTDB with Rama.
Until we meet again, perhaps at the equinox. Or at the conj. I've got my ticket already.
Two free t-shirt ideas: