<?xml version="1.0" encoding="UTF-8"?><?xml-stylesheet href="/rss/styles.xsl" type="text/xsl"?><rss version="2.0" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/"><channel><title>Phil Denhoff</title><description>stuff i think is worth writing about</description><link>https://denhoff.ca/blog</link><language>en-ca</language><lastBuildDate>Mon, 10 Feb 2025 00:00:00 GMT</lastBuildDate><atom:link href="https://denhoff.ca/rss.xml" rel="self" type="application/rss+xml"/><ttl>1440</ttl><image><url>https://denhoff.ca/favicon-96x96.png</url><title>Phil Denhoff</title><link>https://denhoff.ca/blog</link><width>96</width><height>96</height></image><item><title>Using Rust wasn&apos;t a mistake</title><link>https://denhoff.ca/posts/citadel-rust</link><guid isPermaLink="true">https://denhoff.ca/posts/citadel-rust</guid><description>But it was a painful experience.</description><pubDate>Mon, 10 Feb 2025 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;It was a painful experience.&lt;/p&gt;
&lt;p&gt;I originally created &lt;a href=&quot;https://citadel.page/&quot;&gt;Citadel&lt;/a&gt; as a &lt;a href=&quot;https://tauri.app/&quot;&gt;Tauri&lt;/a&gt; app, using Svelte for the frontend and Rust in the backend.
At the start, this worked pretty well — I enjoyed learning about Rust and how it worked, managing references, and writing “performant” software.&lt;/p&gt;
&lt;p&gt;At some point, I looked into running the Rust backend as a separate, stand-alone REST API so that you could self host a web version of Citadel.
While feasible, it seemed painful.
Working with Tokio is annoying for someone whose day job involves writing highly concurrent Elixir and a lot of async Typescript.
Why do I have to manage lifecycles? Why am I giving myself this pain?
After hours of trying to get async requests working across the app, I gave up.
So I shelved the HTTP API and continued working away on Citadel as a desktop app.&lt;/p&gt;
&lt;p&gt;Adding features was slow, steady progress. But it often worked in two phases:&lt;/p&gt;
&lt;ol&gt;
&lt;li&gt;Very quick progress in the UI (using tools I am very familiar with)&lt;/li&gt;
&lt;li&gt;Very, &lt;em&gt;very&lt;/em&gt; slow progress in the small, complicated backend&lt;/li&gt;
&lt;/ol&gt;
&lt;p&gt;Every time I wanted to retrieve more data from the existing database schema
(Citadel uses an existing schema, derived from Calibre), I had to remember how
Rust and the libraries I’m using work.
Then, I had to figure out what business rules Calibre expects / follows and then
mesh the two to get data out — and the process is even slower when trying to
insert new data.
All the inherit complexity got lost in the incidental complexity of using Rust.&lt;/p&gt;
&lt;p&gt;About a month ago, I came back to Citadel with the plan to start supporting downloading metadata from sources like &lt;a href=&quot;https://hardcover.app/&quot;&gt;Hardcover&lt;/a&gt;.
Calibre supports this (using other providers), Calibre-Web supports this, Citadel needs to.
I first tried to make a super-simple prototype in the frontend, but Hardcover blocks requests from localhost, so I had to push it to the backend.&lt;/p&gt;
&lt;p&gt;After three hours of trying to get Rust to make a web request in a reasonable, understandable way, I gave up.
I doubt Rust is to blame here, but I could not figure out how to use an async runtime like Tokio when making IPC requests via Tauri.
The lifetimes got all messed up.
I couldn’t find a sync HTTP library, either. I don’t need async HTTP requests! I don’t really care! But the ecosystem has moved to async, and bring your own runtime.&lt;/p&gt;
&lt;p&gt;In comparison, the same graphql request in Typescript is trivially easy.
I mean, of course it is.
There are tradeoffs to that simplicity.&lt;/p&gt;
&lt;p&gt;But after looking at the options for Citadel, I’ve landed on migrating the app to Electron.
ugh, I know.
&lt;em&gt;Electron&lt;/em&gt;.
Bane of users everywhere, with its non-native UI patterns and 300MB executables.
It’s gross! Frankly, I hate it!
I started Citadel originally as a Swift app exactly to avoid this problem.
I wanted fast, native software.
But the pain of not being able to make quick progress because I was learning new
tooling outran the joy of learning new tools eventually, and now I’d trade
anything to be able to release the features I wanted to add to Citadel &lt;em&gt;over a
year ago&lt;/em&gt; but couldn’t, because I wasn’t comfortable with my tools.&lt;/p&gt;
&lt;p&gt;I’m in do-or-die mode with Citadel.
If I can’t start making progress and adding features that I want, I’ll probably
stop using it.
At that point, who cares if it’s built with Electron or Neutralino or Tauri or
Wails or if its native software?
This is a hobby project, which I wrote &lt;em&gt;for me&lt;/em&gt;.
I probably have over a gigabyte of intermediate build artifacts for the Rust build.
At this point, trading to an Electron build might &lt;em&gt;save me disk space&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;So, while I enjoyed the satisfaction and challenge of writing Rust code,
I’m dropping the language from Citadel for now.
Maybe a new project will come up where Rust feels like the right choice?&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>citadel</category><category>rust</category></item><item><title>Applying &quot;tradeoffs&quot; across your life</title><link>https://denhoff.ca/posts/training-search</link><guid isPermaLink="true">https://denhoff.ca/posts/training-search</guid><description>Dog ownership has been a series of finding and resolving trade-offs. I didn&apos;t expect that.</description><pubDate>Mon, 07 Oct 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve recently been practicing pragmatism more, searching to find good balances
in trade-offs. One very positive side effect from that is how it’s changed my
feelings towards dog ownership.&lt;/p&gt;
&lt;p&gt;My border collie / aussie shepherd, Search, needs a lot of active brain time.
He always needs a job, he needs a good amount of training every day. For a
long time, I felt guilty when I didn’t have the energy to satisfy those needs.&lt;/p&gt;
&lt;p&gt;I see it differently now. It’s a trade-off. Every day for Search should be a
good day. Not every day can be a great day, and if I don’t have the energy
today, maybe I can find it tomorrow. Does he really need a park day today, or
did he burn off enough energy yesterday? Can we do a longer park day today, and
do something different tomorrow? How can I make today a great day for him?&lt;/p&gt;
&lt;p&gt;It’s taken such a load off my shoulders. I do want every day to be amazing for
him, but he’s a dog. He’ll survive a day or two without crazy active brain
time. Actually, he’ll certainly survive a lot of days, but I don’t want to try
that.&lt;/p&gt;
&lt;p&gt;So I just keep trying to find that balance, every day, where he gets his needs
met and I’m doing what I enjoy, and not making dog ownership a chore.&lt;/p&gt;
&lt;p&gt;❦&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>search (the dog)</category><category>dog ownership</category><category>tradeoffs</category></item><item><title>On Prototyping</title><link>https://denhoff.ca/posts/prototyping</link><guid isPermaLink="true">https://denhoff.ca/posts/prototyping</guid><description>Write throw away prototypes to learn new things.</description><pubDate>Fri, 16 Aug 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Over the last month or so, I’ve had two opportunities at my job to build prototypes of existing &lt;em&gt;things&lt;/em&gt; that I have extensive experience with.
Both prototypes have been extremely helpful in teaching me how the existing systems work.
Without building the thing haphazardly and hackily, I wouldn’t understand first-hand the tradeoffs that were made for our existing &lt;em&gt;things&lt;/em&gt;.&lt;/p&gt;
&lt;p&gt;One prototype gave me the opportunity to learn about the kind of HTTP server Middleware that you only write when first setting up and API.
Not a common thing, by any means.
It taught me about a new HTTP server library, and how to delicately balance storing information in the context of that request in a way that was actually usable downstream.&lt;/p&gt;
&lt;p&gt;The other project taught me about the complexities of orchestrating background job processing, similar to Elixir’s Oban.
I learned about Web Workers and their limitations (each is on its own thread, so there’s no shared objects; messages passed between Workers must be contain only primitives, no objects or functions).
I practiced writing code that doesn’t crash when things go wrong — crashing in a Web Worker crashes the Bun runtime, which means your server shutdown because a worker failed.
That’s almost the antithesis to the Elixir/BEAM runtime — Erlang virtual processess can crash all the time but the OS &lt;em&gt;runtime&lt;/em&gt; process will never crash.&lt;/p&gt;
&lt;p&gt;When would I need to learn the ins and outs of background job processing?
How often does one set up a new HTTP API and need to set it up from scratch?
These things don’t come up unless you give them the opportunity.&lt;/p&gt;
&lt;p&gt;Create prototypes.
Write crappy, throw away code.
What if, one day, you want to write a production-ready version?
Now you’ve learned the basics, now you can do it.
You’ve already made mistakes you can learn from.&lt;/p&gt;
&lt;p&gt;You just have to go out and build it.&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>learning</category><category>programming</category></item><item><title>The Value of Legacy Software</title><link>https://denhoff.ca/posts/on-legacy-code</link><guid isPermaLink="true">https://denhoff.ca/posts/on-legacy-code</guid><description>&quot;Legacy&quot; code is, unfortunately, valuable</description><pubDate>Fri, 19 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve recently been working on a piece of software, about 10 years old, that is a Shopify app.&lt;/p&gt;
&lt;p&gt;It was acquired from an individual who had evidently built it haphazardly and, when bought, the app was heavily invested into.
Much was done to bring the UI up to snuff with Shopify’s expectations for good software.
Flow (the now-defunct competitor to Typescript) was added. Storybook and other niceties as well.&lt;/p&gt;
&lt;p&gt;However, behind all of the UI cleanup, the addition of types in some spots, and a few new features lurks a ball of mud.
A ball of mud essentially written by one man, and now maintained by a small team.
We add the occassional feature here, and we might fix a bug there, but most of our work on the &lt;strong&gt;existing&lt;/strong&gt; code is reactionary: we &lt;em&gt;must&lt;/em&gt; make this change.
Shopify is deprecating an API and now &lt;em&gt;we must&lt;/em&gt; rewrite the existing API client to use the new API.
We are adding new functionality here to sell our product to new markets so &lt;em&gt;we must&lt;/em&gt; change this 8-year-old, 800 line function.&lt;/p&gt;
&lt;p&gt;What we are &lt;strong&gt;not&lt;/strong&gt; doing is &lt;em&gt;opportunistically&lt;/em&gt; improving our legacy code.
When adding another layer on top of existing code, we haven’t asked ourselves how we can improve what is already there.
Can we get the method under test?
Can we split up the method to get some major fraction of it under test?
We simply bolt code onto the outside, try to futz as little as possible with the inside bits, and move on.&lt;/p&gt;
&lt;p&gt;I don’t think this is healthy for a team, and more importantly, not healthy for a business.
When you continue to bolt things onto the outside, you live in a &lt;em&gt;shanty town&lt;/em&gt;. You build a ball of mud.
Every architecture decision is you, your team, and the existing code that you cannot refactor if you wanted to.
The code ends up making more architectural decisions than you do.
And, because you cannot control the code, you cannot add the features that you want to.
You cannot achieve the timelines that you want to.&lt;/p&gt;
&lt;p&gt;So why have a ball of mud? Because it’s profitable.
Today, that is the hardest pill for me to swallow, but it’s what I’m learning to accept about businesses.
It’s simply not necessary to clean up our work; we can continue making money without changing the ball of mud.
Even better, not changing the code minimizes the short-term risk that our changes might break something.
Heaven forbid our profitable, unassuming product should run into issues.&lt;/p&gt;
&lt;p&gt;What that calculus fails to include is the outsized risk of being prevented from making changes, or slowed down when you can.
If a product is in maintenance mode, not touching legacy code is fine.
It continues to make money, the occasional bug is squashed, and required changes are made.
But when you want to extend a product, in functionality or in market share, you need to be willing to dry out some mud.&lt;/p&gt;
&lt;p&gt;❦&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>maintenance</category><category>legacy code</category></item><item><title>golink xsrf error</title><link>https://denhoff.ca/posts/go-xsrf</link><guid isPermaLink="true">https://denhoff.ca/posts/go-xsrf</guid><description>Tailscale&apos;s golinks is excellent. XSRF errors are not.</description><pubDate>Thu, 11 Apr 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;A recent update of Tailscale’s excellent &lt;a href=&quot;https://github.com/tailscale/golink&quot;&gt;golink&lt;/a&gt; tool broke creating links for me.&lt;/p&gt;
&lt;p&gt;Every time I tried to create a new link, I got an error: “invalid xsrf token”.&lt;/p&gt;
&lt;p&gt;I tried restarting my browser, going straight to &lt;a href=&quot;https://go/&quot;&gt;https://go/&lt;/a&gt; instead of the
usual &lt;a href=&quot;https://go.my-tailnet.ts.com/&quot;&gt;https://go.my-tailnet.ts.com&lt;/a&gt;, opening golink in a different browser
(Safari, instead of Arc), but all to no avail.&lt;/p&gt;
&lt;p&gt;So, I rolled back my Docker container to the previous release (see below).
Rolling back, suddenly the app worked again. Great! That makes sense, since the
most recent release includes changes to how XSRF/CSRF is handled when adding
new links.&lt;/p&gt;
&lt;p&gt;However, I rolled forward again, to the latest image. And the app works again
normally. I don’t know why. If you run into the same issue as I did with
golinks, maybe you need to restart the service? Or roll back &amp;amp; roll forward?&lt;/p&gt;
&lt;p&gt;I hope that helps!&lt;/p&gt;
&lt;hr /&gt;
&lt;p&gt;The version I rolled back to:&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/tailscale/golink/pkgs/container/golink/191714519&quot;&gt;ghcr.io/tailscale/golink@sha256:09c8f79a4b7f908337888e0cb4e2c24ec6bbeadb22b9385fefd9f6ed29f5adfe&lt;/a&gt;,
which corresponds to &lt;a href=&quot;https://github.com/tailscale/golink/commit/6c79b503e19a500c551ce162b0a1b32abebd9a94&quot;&gt;this commit&lt;/a&gt;.&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>tailscale</category><category>xsrf</category><category>golink</category></item><item><title>Replacing Calibre</title><link>https://denhoff.ca/posts/replacing-calibre</link><guid isPermaLink="true">https://denhoff.ca/posts/replacing-calibre</guid><description>Calibre is a powerful tool. It&apos;s time to build a better Calibre.</description><pubDate>Sat, 13 Jan 2024 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;Calibre is an excellent tool — if you believe that a swiss army knife with 95 attachments is an excellent tool.&lt;/p&gt;
&lt;figure&gt;&lt;img src=&quot;https://denhoff.ca/assets/posts/replacing-calibre/calibre-pocket-knife.webp&quot; width=&quot;512&quot; height=&quot;512&quot; alt=&quot;A pocket knife with way too many goddamn tools. It&apos;s useless!&quot; /&gt;&lt;figcaption&gt;&lt;p&gt;Calibre, pretty much.&lt;/p&gt;&lt;/figcaption&gt;&lt;/figure&gt;
&lt;p&gt;However, if you enjoy being able to reach into your bag, grab your pocket knife and immediately know how to use it, Calibre is the worst tool you could use.
That’s why I’ve been writing a backwards compatible replacement for Calibre.
It’s called &lt;a href=&quot;https://citadel.page/&quot;&gt;Citadel&lt;/a&gt;, and here’s a short demo video.&lt;/p&gt;
&lt;video controls width=&quot;730&quot; height=&quot;539&quot; poster=&quot;https://denhoff.ca/assets/posts/replacing-calibre/video-poster.webp&quot;&gt;&lt;source type=&quot;video/mp4&quot; src=&quot;https://github.com/everydaythingssoftware/citadel/assets/17505728/84a0c9dd-f14e-411a-8947-1d599f3ad85a&quot;&gt;&lt;/source&gt;&lt;a href=&quot;https://github.com/everydaythingssoftware/citadel/assets/17505728/84a0c9dd-f14e-411a-8947-1d599f3ad85a&quot;&gt;Watch the Citadel demo video (MP4).&lt;/a&gt;&lt;/video&gt;
&lt;p&gt;In case you didn’t notice, it’s quite rough!&lt;/p&gt;
&lt;h2&gt;Citadel, a prototype replacement for Calibre&lt;/h2&gt;
&lt;p&gt;Citadel is a Tauri desktop app that reads &amp;amp; writes Calibre libraries (SQLite database + existing book files) and helps you manage them.
It has a Svelte frontend (that can be run over the internet) and a Rust backend (which supports headless HTTP server mode).
Most of the work to read &amp;amp; write Calibre data is in a separate crate called &lt;code&gt;libcalibre&lt;/code&gt;, which I hope to separately publish in the future so that others can create robust Calibre tools.&lt;/p&gt;
&lt;p&gt;So far, you can:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;List all of your books as a cover view or table view&lt;/li&gt;
&lt;li&gt;Add a new book&lt;/li&gt;
&lt;li&gt;Edit the title of a book&lt;/li&gt;
&lt;li&gt;Copy a book to a folder on your computer, e.g. a SD card that’s attached to your computer (for ereaders)&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;As you can tell, this is &lt;em&gt;incredibly&lt;/em&gt; early software with little functionality and a propensity to crash if you look at it wrong (I have a few too many panic-y &lt;code&gt;.unwrap()&lt;/code&gt;s).&lt;/p&gt;
&lt;p&gt;It’s early days for the project! Calibre has been around for 15 years and has ~450k lines of Python.
Citadel has maybe ~3k lines of Rust, and maybe another 1k lines of Svelte/Typescript.&lt;/p&gt;
&lt;p&gt;But it’s a prototype, and it’s working today, and I’m slowly replacing my Calibre usage with an app I wrote myself that doesn’t take 8s to boot on an M2 Max Macbook Pro.&lt;/p&gt;
&lt;p&gt;&lt;a href=&quot;https://github.com/everydaythingssoftware/citadel&quot;&gt;Citadel is open-source and on GitHub&lt;/a&gt;. There are no release builds yet and it’s only been tested on my M2 Macbook.
It &lt;em&gt;should&lt;/em&gt; work elsewhere, but if you run into issues please let me know!&lt;/p&gt;
&lt;h2&gt;Addendum&lt;/h2&gt;
&lt;h4&gt;Why Svelte + Rust?&lt;/h4&gt;
&lt;p&gt;I picked Rust for the backend because I wanted to learn it. This is a hobby project! I’m allowed to do whatever I want.&lt;/p&gt;
&lt;p&gt;For the frontend, I wanted to use something that isn’t React, having been using it at work for 2+ years.
I know Vue &amp;amp; Svelte pretty well, and I wanted another project to use Svelte in (besides my &lt;a href=&quot;https://github.com/phildenhoff/logseq-raindrop&quot;&gt;Logseq/Raindrop&lt;/a&gt; plugin and my &lt;a href=&quot;https://github.com/phildenhoff/logseq-slipbox&quot;&gt;Logseq slipbox&lt;/a&gt; plugin), so that’s why it’s a Svelte frontend.
It’s specifically web-tech because I already know how to write web frontends. I tried Swift before! But it took too long to make progress.&lt;/p&gt;
&lt;p&gt;With a native backend, a fast-to-write-and-maintain frontend, the use of native webviews (the binary for macOS is 18MB), and the option of splitting the frontend &amp;amp; backend up to connect them over HTTP, going down this route has been a great choice so far.&lt;/p&gt;
&lt;h2&gt;A note on &lt;code&gt;libcalibre&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;I’ve separated all of the logic of “how to talk to a Calibre library” into a crate called &lt;code&gt;libcalibre&lt;/code&gt;.
It’s (experimentally) written applying DDD, but I’m not sure how well I like that.&lt;/p&gt;
&lt;p&gt;The crate itself is pretty flexible — you should be able to use it in a CLI tool, or compile it for another language and make a &lt;em&gt;true&lt;/em&gt; native e-book library management app.
&lt;a href=&quot;https://github.com/phildenhoff/native-macos-digital-library&quot;&gt;I started doing that before, in Swift&lt;/a&gt;, before realising that I was working to learn both Swift as well as all of Calibre’s business logic, which made progress infuriatingly slow.&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>calibre</category><category>rust</category><category>svelte</category><category>tauri</category><category>learning in public</category></item><item><title>Android won&apos;t get passkeys from other devices... unless you tell it what to get</title><link>https://denhoff.ca/posts/webauthn-by-id-android</link><guid isPermaLink="true">https://denhoff.ca/posts/webauthn-by-id-android</guid><description>I just want to sign into my dang app. Why do I have to give you the list of valid credentials?? Let the user pick one!</description><pubDate>Wed, 27 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been playing around with WebAuthn recently, building an app that uses it as the primary login method.
Building out the backend has been interesting — I’m constantly learning new stuff — but the frontend has been exhausting.&lt;/p&gt;
&lt;p&gt;Here’s one example. I’ll try to keep it short.&lt;/p&gt;
&lt;p&gt;In our flow, we want users to sign in by their username &amp;amp; passkey in one step.
Those passkeys might be stored in the platform authenticator or in a remote authenticator: think Android/iOS vs. iCloud/1Password/Bitwarden/another phone.&lt;/p&gt;
&lt;p&gt;On Android, if you make a &lt;code&gt;navigator.credentials.get&lt;/code&gt; call &lt;em&gt;without&lt;/em&gt; providing a list of allowed credentials in the &lt;code&gt;allowCredentials&lt;/code&gt; option, we will &lt;strong&gt;only get back the platform credentials&lt;/strong&gt;.
If no platform credentials have been set up for the relaying party’s id (&lt;code&gt;rpId&lt;/code&gt;), the user will be told they have none, and given no other info or options.
Android won’t ask the user if they’d like to use a remote authenticator, like their hardware token or a different phone.&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// Android users will not be able to use a remote authenticator for this credential request
await navigator.credentials.get({publicKey: {
	challenge: ArrayBuffer,
    timeout: SIXTY_SECONDS,
    rpId: window.location.hostname,
    allowCredentials: [],
    userVerification: preferred
}});&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;However, if we &lt;em&gt;do&lt;/em&gt; provide a list of allowed credentials, Android will prompt only for &lt;strong&gt;USB, Bluetooth or NFC devices&lt;/strong&gt; — I guess it doesn’t support scanning QR codes from other phones, or password managers?&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-ts&quot;&gt;// Android users can use their BT, NFC, or USB authenticators for this.
// But not other phones.
await navigator.credentials.get({publicKey: {
	challenge: ArrayBuffer,
    timeout: SIXTY_SECONDS,
    rpId: window.location.hostname,
    allowCredentials: [{
      id: RemotelyStoredCredentialId,
      type: &quot;public-key&quot;,
      transports: []
    }],
    userVerification: preferred
}});&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;On iOS, either call works — with or without the list of allowed credentials.
The first will look for valid credentials for the &lt;code&gt;rpId&lt;/code&gt;, suggest them, &lt;strong&gt;and provide an alternative option to use a remote authenticator&lt;/strong&gt;.
The second call, with the list of allowed credentials, will filter out anything local not in that list and, finding that there are no local credentials, suggest using a remote authenticator.&lt;/p&gt;
&lt;p&gt;Why is Android like this? I don’t know.&lt;/p&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>webauthn</category><category>android</category><category>ios</category><category>learning in public</category></item><item><title>Using Nx to build ReScript libraries for TypeScript apps</title><link>https://denhoff.ca/posts/rescript-nx</link><guid isPermaLink="true">https://denhoff.ca/posts/rescript-nx</guid><description>I want to write my library in ReScript, but use it in a Typescript/Node app. How do I set it up?</description><pubDate>Fri, 22 Sep 2023 00:00:00 GMT</pubDate><content:encoded>&lt;p&gt;I’ve been playing around with &lt;a href=&quot;https://nx.dev/&quot;&gt;Nx&lt;/a&gt; recently.
At my last job, I migrated all of our Javascript/Typescript code to a monorepo using Nx, and I loved the experience, but I often wondered what it would be like to use other compiles-to-JS languages.&lt;/p&gt;
&lt;p&gt;Well, I created &lt;a href=&quot;https://github.com/phildenhoff/nx-with-functional-languages&quot;&gt;nx-with-functional-languages&lt;/a&gt; as a kind of public testing ground, to see what the experience would be like with different languages.
It took a decent amount of fiddling to get anything working, and I’m still not in love with what I came up with, so I thought I would share what I did in the hopes of inspiring others to extend my work.&lt;/p&gt;
&lt;p&gt;Let’s see how we can set up a Node/Express server that uses a &lt;a href=&quot;https://rescript-lang.org/&quot;&gt;ReScript&lt;/a&gt; library.
ReScript is a strictly-typed language with some niceties over JS, like pipes and pattern matching, but most importantly it’s an example language with its own build system that can generate Javascript (and Typescript) that we can import from our Typescript project.
Let’s see what using it looks like.&lt;/p&gt;
&lt;h2&gt;Creating the Nx workspace&lt;/h2&gt;
&lt;p&gt;&lt;em&gt;Side note&lt;/em&gt;: You can &lt;a href=&quot;https://github.com/phildenhoff/rescript-and-node-nx&quot;&gt;view the final repo on GitHub&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;First, we need to create an empty Nx workspace for us to try all this out in.
I’m using &lt;a href=&quot;https://pnpm.io/&quot;&gt;pnpm&lt;/a&gt; here, but you should be able to use yarn, npm, or bun.
&lt;code&gt;pnpm dlx&lt;/code&gt; is the same as &lt;code&gt;yarn create&lt;/code&gt;, &lt;code&gt;npx&lt;/code&gt; or &lt;code&gt;bunx&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;I’ll call my example workspace &lt;code&gt;org&lt;/code&gt; — feel free to use something else, but where I write &lt;code&gt;@org&lt;/code&gt; you’ll need to replace it with your workspace name.&lt;/p&gt;
&lt;p&gt;(You can skip distributed caching, but for real projects I recommend enabling it.)&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm dlx create-nx-workspace@latest &lt;mark&gt;org&lt;/mark&gt; --preset=ts&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;To get an overview of what we’ve just created,&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cd org
ls
git log --oneline
git status&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h3&gt;Creating an application and a library package&lt;/h3&gt;
&lt;p&gt;We’ll need to create some library code, which we’ll later reconfigure as a Rescript project, as well as an application that can use that code.
This is the core of Nx — package &amp;amp; dependency management, with some extra caching.&lt;/p&gt;
&lt;p&gt;A Node.js server will work well as the application, and Nx has a generator we can run to automatically set that up for us.
Rescript is not yet supported (thus, this article) and so we’ll have to hack on top of what we get out of the &lt;code&gt;@nx/js&lt;/code&gt; generator to get that working.
I don’t want to delete/unconfigure a bunch of extra crap, so ideally we limit the amount of stuff Nx configures for us — no bundler, no test runner, no linting.
However, the only way I figured out how all this works was by generating projects with different configs, so I do recommend giving it a go!
Just… not for this walkthrough.&lt;/p&gt;
&lt;p&gt;Okay, enough preamble, how do we create a nearly-empty JS library that we can hack on?&lt;/p&gt;
&lt;h4&gt;Creating a basic library&lt;/h4&gt;
&lt;p&gt;We’ll go with a library name of &lt;code&gt;finance&lt;/code&gt;.
If, while running this command, you are asked “What should be the project name and where should it be generated”, pick libs/finance.&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm nx generate @nx/js:library --name=&lt;mark&gt;finance&lt;/mark&gt; \
  --unitTestRunner=none \
  --bundler=none \
  --linter=none \
  --directory=libs/&lt;mark&gt;finance&lt;/mark&gt; \
  --importPath=@org/&lt;mark&gt;finance&lt;/mark&gt;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;With that done (I recommend committing your changes along the way, but I won’t remind you at every step), we need to add some more missing stuff.&lt;/p&gt;
&lt;p&gt;Create a &lt;code&gt;package.json&lt;/code&gt; like this, in &lt;code&gt;libs/finance&lt;/code&gt;:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// libs/finance/package.json
{
  &quot;name&quot;: &quot;@org/finance&quot;,
  &quot;version&quot;: &quot;0.0.1&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;dependencies&quot;: {},
  &quot;devDependencies&quot;: {}
}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;Ensure the type is &lt;code&gt;module&lt;/code&gt;&lt;/strong&gt;, we need that for import resolution later, in our app.&lt;/p&gt;
&lt;h4&gt;Creating a Node.js app&lt;/h4&gt;
&lt;p&gt;There’s an Nx generator for Node apps in the &lt;code&gt;@nx/node&lt;/code&gt; package, but we don’t have that yet.
Install it as a dev dependency (since only developers need it, to generate Node projects):&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm add --save-dev @nx/node&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Now we can use it: we’ll choose a project name of &lt;code&gt;api&lt;/code&gt; and the webserver framework Express.
You can use any web framework you like, but you may need to make other changes for this walkthrough.&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm nx g @nx/node:application --name=&lt;mark&gt;api&lt;/mark&gt; --framework=express&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;h4&gt;Dependent builds &amp;amp; bundling&lt;/h4&gt;
&lt;p&gt;We need to make some changes to how Nx has set up our Node application — by default, when we build our server it won’t automatically build code from monorepo packages it’s dependent on, and the generated file won’t properly reference our &lt;code&gt;finance&lt;/code&gt; library.&lt;/p&gt;
&lt;p&gt;First, we want to make sure that when we build our Node.js app, Nx goes and builds everything that it depends on first.
Within &lt;code&gt;api/project.json&lt;/code&gt;, add &lt;code&gt;&quot;dependsOn&quot;: [&quot;^build&quot;]&lt;/code&gt; to the build target.&lt;/p&gt;
&lt;p&gt;Second, we need to make sure that when we build &lt;code&gt;api&lt;/code&gt; that the build tool (esbuild, by default) can reference the packages our code will import.
The configuration for how we use esbuild is in the Nx config file &lt;code&gt;api/project.json&lt;/code&gt; — specifically, within the “build” target.
If we left &lt;code&gt;&quot;bundle&quot;: false&lt;/code&gt; in our esbuild config, we would end up with a generated file (&lt;code&gt;dist/api/main.js&lt;/code&gt;) that attempts to &lt;code&gt;require()&lt;/code&gt; from a package &lt;code&gt;@org/finance&lt;/code&gt; that cannot be found (Why, I’m not sure. Maybe because we generate a new package.json in our esbuild config?).
If we don’t generate the &lt;code&gt;package.json&lt;/code&gt;, the build fails to import &lt;code&gt;express&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;So, we need to ensure that our generated file can reference its imports.
One way of doing that is by &lt;strong&gt;bundling all of the imports into one giant file&lt;/strong&gt;, which is what we’ll do.
This is less than ideal — debugging is likely to be more difficult unless you correctly set up source maps.
What we’re doing here is &lt;strong&gt;fine for a prototype&lt;/strong&gt;, but a more complex system might have issues.
If you come up with a better solution, please let me know!&lt;/p&gt;
&lt;p&gt;Third, we want to make sure we’re set up to use ES Modules, by changing the &lt;code&gt;format&lt;/code&gt; from &lt;code&gt;cjs&lt;/code&gt; to &lt;code&gt;esm&lt;/code&gt;.
Whether we &lt;em&gt;need&lt;/em&gt; to use ES Modules is unclear to me, but I had issues importing Common JS modules which were resolved when I swapped to ESM.&lt;/p&gt;
&lt;p&gt;Our &lt;code&gt;api/project.json&lt;/code&gt; file should end up looking like this:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// api/project.json
{
  &quot;name&quot;: &quot;api&quot;,
  &quot;sourceRoot&quot;: &quot;api/src&quot;,
  &quot;projectType&quot;: &quot;application&quot;,
  &quot;targets&quot;: {
    &quot;build&quot;: {
      &quot;executor&quot;: &quot;@nx/esbuild:esbuild&quot;,
      &quot;outputs&quot;: [&quot;{options.outputPath}&quot;],
      &quot;defaultConfiguration&quot;: &quot;production&quot;,
      &quot;dependsOn&quot;: [&quot;^build&quot;],
      &quot;options&quot;: {
        &quot;platform&quot;: &quot;node&quot;,
        &quot;outputPath&quot;: &quot;dist/api&quot;,
        &quot;format&quot;: [&quot;esm&quot;],
        &quot;bundle&quot;: true,
        // ... snip
        }
      },
      // ... snip
      }
    }
    // ... snip
  },
  &quot;tags&quot;: []
}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Finally, update your &lt;code&gt;api/tsconfig.app.json&lt;/code&gt; to have &lt;code&gt;&quot;module&quot;: &quot;esnext&quot;&lt;/code&gt;, as we use ES Modules in our libraries.&lt;/p&gt;
&lt;h2&gt;Using ReScript in &lt;code&gt;@org/finance&lt;/code&gt;&lt;/h2&gt;
&lt;p&gt;Our goal is to use ReScript’s build system to build our source code, but wrap it in Nx to get the benefits of &lt;a href=&quot;https://nx.dev/concepts/how-caching-works&quot;&gt;caching&lt;/a&gt; and &lt;a href=&quot;https://nx.dev/concepts/more-concepts/dependency-management&quot;&gt;dependency management&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;We’ll follow the (short) ReScript install guide to get it set up in our project, then create some ReScript source code files, build them by calling the ReScript build system directly, and then wrap that in an Nx task to support caching &amp;amp; dependency management.&lt;/p&gt;
&lt;h3&gt;Rescript installation&lt;/h3&gt;
&lt;p&gt;In the project root, run&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;pnpm add rescript&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;We also need to add the ReScript version we just installed into our &lt;code&gt;libs/finance/package.json&lt;/code&gt;.
We can get the version with&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cat package.json | grep rescript&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;And then add it to &lt;code&gt;libs/finance/package.json&lt;/code&gt;.
&lt;strong&gt;Generally, we should not have to do this.&lt;/strong&gt;
If we had an Nx/ReScript plugin, Nx would manage our package.json dependencies automatically for us by inspecting the source code and seeing which dependencies are used.&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// libs/finance/package.json
{
  &quot;name&quot;: &quot;@org/finance&quot;,
  &quot;version&quot;: &quot;0.0.1&quot;,
  &quot;type&quot;: &quot;module&quot;,
  &quot;dependencies&quot;: {
    &quot;rescript&quot;: &quot;^10.1.4&quot;
  },
  &quot;devDependencies&quot;: {}
}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Finally, we have to make sure it’s installed in the node_modules in &lt;code&gt;libs/finance&lt;/code&gt; by running &lt;code&gt;pnpm install&lt;/code&gt; in that folder:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-shell&quot;&gt;cd libs/finance
pnpm install&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Next, we’ll create a Rescript config file (“bsconfig.json”) in &lt;code&gt;libs/finance&lt;/code&gt;.
Use the config below, which is based on the &lt;a href=&quot;https://rescript-lang.org/docs/manual/latest/installation#integrate-into-an-existing-js-project&quot;&gt;recommended config&lt;/a&gt; but includes a &lt;code&gt;gentypeconfig&lt;/code&gt; &lt;a href=&quot;https://rescript-lang.org/docs/manual/latest/build-configuration-schema#:~:text=gentypeconfig&quot;&gt;directive&lt;/a&gt; to generate Typescript types when we use the &lt;code&gt;@genType&lt;/code&gt; annotation.
You can &lt;a href=&quot;https://rescript-lang.org/docs/gentype/latest/introduction&quot;&gt;read more about Rescript’s GenType in their docs&lt;/a&gt;.&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// libs/finance/bsconfig.json
{
  &quot;name&quot;: &quot;finance&quot;,
  &quot;sources&quot;: [
    {
      &quot;dir&quot;: &quot;src&quot;,
      &quot;subdirs&quot;: true
    }
  ],
  &quot;package-specs&quot;: [
    {
      &quot;module&quot;: &quot;es6&quot;,
      &quot;in-source&quot;: true
    }
  ],
  &quot;suffix&quot;: &quot;.bs.js&quot;,
  &quot;bs-dependencies&quot;: [],
  &quot;gentypeconfig&quot;: {
    &quot;shims&quot;: {},
    &quot;generatedFileExtension&quot;: &quot;.gen.ts&quot;,
    &quot;module&quot;: &quot;es6&quot;,
    &quot;debug&quot;: {
      &quot;all&quot;: false,
      &quot;basic&quot;: false
    }
  }
}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;We want to ensure that we’re using ES Modules, so update &lt;code&gt;libs/finance/tsconfig.json&lt;/code&gt; to include &lt;code&gt;module: &quot;esnext&quot;&lt;/code&gt;, just like we did for our &lt;code&gt;api&lt;/code&gt; application.&lt;/p&gt;
&lt;h3&gt;Our first ReScript source code&lt;/h3&gt;
&lt;p&gt;Our project is now set up for us to write some basic ReScript source code, commit it, compile to JavaScript &amp;amp; TypeScript, and commit that as well.
You might be wondering, “why would we commit our build artifacts?”
&lt;a href=&quot;https://rescript-lang.org/docs/manual/latest/interop-with-js-build-systems#popular-js-build-systems&quot;&gt;ReScript recommends it for a number of reasons, including ease of adoption&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Delete &lt;code&gt;libs/finance/src/lib&lt;/code&gt; — we don’t need it.&lt;/p&gt;
&lt;p&gt;Create a new ReScript file &lt;code&gt;libs/finance/src/TimeValue.res&lt;/code&gt;, with the following sample function:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-rescript&quot;&gt;// libs/finance/src/TimeValue.res
/**
  * Compute the future value of an investment using compounding interest.
  * For some principal investment pv, interest rate i, and number of periods n,
  * returns the future value of the investment.
  *
  * @example
  * fv(1000, 0.0675, 20) // =&amp;gt; 2896.703652
  */
@genType
let compoundInterest = (pv, i: float, n): float =&amp;gt; pv *. (1.0 +. i) ** n&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;We won’t go over the details of &lt;code&gt;compoundInterest&lt;/code&gt; — feel free to use any sample function here.&lt;/p&gt;
&lt;p&gt;Now, compile your ReScript file to JS &amp;amp; TS by running &lt;code&gt;pnpm rescript build&lt;/code&gt; while in the &lt;code&gt;libs/finance&lt;/code&gt; directory.
You should see two new files in &lt;code&gt;libs/finance/src&lt;/code&gt;: &lt;code&gt;TimeValue.bs.js&lt;/code&gt; and &lt;code&gt;TimeValue.gen.ts&lt;/code&gt;.
This is what our &lt;code&gt;api&lt;/code&gt; application will use.
However, it’s not yet available as an export from our library.&lt;/p&gt;
&lt;p&gt;&lt;strong&gt;Note&lt;/strong&gt;: you’ll also see a new &lt;code&gt;libs/finance/lib&lt;/code&gt; folder — you can .gitignore this.
It’s build information for ReScript, but we don’t need to keep it around AFAIK.&lt;/p&gt;
&lt;p&gt;To make the exports available, we’ll add them to our &lt;code&gt;libs/finance/index.ts&lt;/code&gt;. Update it to this:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;// libs/finance/index.ts
export * from &apos;./TimeValue.gen&apos;;&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Here, we’re exporting &lt;em&gt;only&lt;/em&gt; the TypeScript code, to ensure that downstream callees call the right function.
We could &lt;em&gt;instead&lt;/em&gt; export from “TimeValue.bs”, but that would export only the JavaScript code, which is not really what we want — it has no type safety.&lt;/p&gt;
&lt;p&gt;Note: do not include the .ts extension.
Our &lt;code&gt;api&lt;/code&gt; build does not support including it.&lt;/p&gt;
&lt;p&gt;With Rescript building our code manually, let’s look at adding our build as an Nx task.&lt;/p&gt;
&lt;h3&gt;Building ReScript with Nx&lt;/h3&gt;
&lt;p&gt;We’re going to use the “nx:run-commands” executor for our build, which allows to run arbitrary shell commands in our target.
This isn’t the Nx Way™ — ideally, we would have a ReScript plugin that controls our build — but that won’t stop us from building a prototype that could be improved later.
With the “nx:run-commands” executor, we can call the command &lt;code&gt;pnpm rescript build&lt;/code&gt; in the finance project, tuning what we cache and when.&lt;/p&gt;
&lt;p&gt;Edit the &lt;code&gt;libs/finance/project.json&lt;/code&gt; to look like this:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-json&quot;&gt;// libs/finance/project.json
{
  &quot;name&quot;: &quot;finance&quot;,
  &quot;$schema&quot;: &quot;../../node_modules/nx/schemas/project-schema.json&quot;,
  &quot;sourceRoot&quot;: &quot;libs/finance/src&quot;,
  &quot;projectType&quot;: &quot;library&quot;,
  &quot;targets&quot;: {
    &quot;build&quot;: {
      &quot;executor&quot;: &quot;nx:run-commands&quot;,
      &quot;inputs&quot;: [
        &quot;{projectRoot}/src/index.ts&quot;,
        &quot;{projectRoot}/src/**.res&quot;
      ],
      &quot;outputs&quot;: [
        &quot;{projectRoot}/src/index.ts&quot;,
        &quot;{projectRoot}/src/**.bs.js&quot;,
        &quot;{projectRoot}/src/**.gen.ts&quot;
      ],
      &quot;options&quot;: {
        &quot;commands&quot;: [
          &quot;pnpm rescript build&quot;
        ],
        &quot;cwd&quot;: &quot;libs/finance&quot;
      }
    }
  },
  &quot;tags&quot;: []
}&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;The major addition is the new &lt;code&gt;build&lt;/code&gt; target, using the &lt;code&gt;nx:run-commands&lt;/code&gt; executor.
We tell Nx that the “inputs” (which files, when changed, invalidate the cache) are all the &lt;code&gt;.res&lt;/code&gt; files in our project, as well as the &lt;code&gt;index.ts&lt;/code&gt;.
At the same time, the generated output files that should be read from the cache (if the inputs match) are all &lt;code&gt;.bs.js&lt;/code&gt; and &lt;code&gt;.gen.ts&lt;/code&gt; files.
We tell Nx to run the &lt;code&gt;pnpm rescript build&lt;/code&gt; command when we run our build task; we could add more commands to the array if we needed to, say, copy files around.
We also set the current working directory (&lt;code&gt;cwd&lt;/code&gt;) of the task to the project’s directory, &lt;code&gt;libs/finance&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Okay, nice! That’s all we need to have Nx run the build of our ReScript source code.
You can run it with &lt;code&gt;pnpm nx build finance&lt;/code&gt; &lt;strong&gt;from the repo root&lt;/strong&gt;.
The build will not work from the &lt;code&gt;libs/finance&lt;/code&gt; directory.&lt;/p&gt;
&lt;p&gt;If you run it again, it should take almost no time as Nx detected the inputs had not changed.
If you keep the index.ts and .res files the same, and delete the .bs.js and .gen.ts files, and re-run the build, it should also take almost no time.
Nx will read the output files out of its cache instead of running the ReScript compiler.&lt;/p&gt;
&lt;h2&gt;Using ReScript code in a Node.js app&lt;/h2&gt;
&lt;p&gt;So, reviewing what we’ve done so far: we have ReScript source code that Nx will build for us.
The build generates TypeScript from ReScript, and our Node.js app can reference that TS.
Why don’t we try it out?&lt;/p&gt;
&lt;p&gt;Open your &lt;code&gt;api/src/main.ts&lt;/code&gt; and import &amp;amp; call &lt;code&gt;compoundInterest&lt;/code&gt;.
You can log the value immediately, or put it in an HTTP response — we’ll do both.
Your file should look like something this:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-typescript&quot;&gt;import express from &apos;express&apos;;
import { compoundInterest } from &apos;@org/finance&apos;;
 
console.log(compoundInterest(100, 0.05, 10));
 
const host = process.env.HOST ?? &apos;localhost&apos;;
const port = process.env.PORT ? Number(process.env.PORT) : 3000;
 
const app = express();
 
app.get(&apos;/&apos;, (req, res) =&amp;gt; {
  const computed = compoundInterest(100, 0.05, 10);
  res.send({ message: computed});
});
 
app.listen(port, host, () =&amp;gt; {
  console.log(`[ ready ] http://${host}:${port}`);
});&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;If we run the server with &lt;code&gt;pnpm nx serve api&lt;/code&gt;, we should see Nx run the &lt;code&gt;@org/finance&lt;/code&gt; build, then start the Node server.
The server should print out the value in the log, and if we get &lt;code&gt;localhost:3000/&lt;/code&gt;, we should see the value in the response.&lt;/p&gt;
&lt;p&gt;The log:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;$ pnpm nx serve api
 
&amp;gt; nx run api:serve:development
 
[ watch ] build succeeded, watching for changes...
Debugger listening on ws://localhost:9229/c73ad56e-881c-490c-95fd-886518b7fad2
Debugger listening on ws://localhost:9229/c73ad56e-881c-490c-95fd-886518b7fad2
For help, see: https://nodejs.org/en/docs/inspector
 
162.88946267774423
[ ready ] http://localhost:3000&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;Via the api:&lt;/p&gt;
&lt;figure&gt;&lt;pre&gt;&lt;code class=&quot;language-log&quot;&gt;$ curl localhost:3000
{&quot;message&quot;:162.88946267774423}⏎&lt;/code&gt;&lt;/pre&gt;&lt;/figure&gt;
&lt;p&gt;&lt;strong&gt;We’re done!&lt;/strong&gt;
Our Node.js app is able to call functions that we wrote in ReScript.
ReScript builds TypeScript &amp;amp; JavaScript code that our Node app’s build system, esbuild, can use.
We use Nx to put both in one repo. It’s just that easy.&lt;/p&gt;
&lt;h2&gt;What now?&lt;/h2&gt;
&lt;p&gt;Here are some ideas for follow-up improvements (please let me know if you do these!):&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Put all of this into a ReScript plugin for Nx&lt;/li&gt;
&lt;li&gt;Find some way to avoid having to bundle all of our server into one file&lt;/li&gt;
&lt;li&gt;Add &lt;em&gt;more&lt;/em&gt; languages. &lt;a href=&quot;https://civet.dev/&quot;&gt;Civet&lt;/a&gt;, maybe?&lt;/li&gt;
&lt;/ul&gt;</content:encoded><dc:creator>Phil Denhoff</dc:creator><category>nx</category><category>rescript</category><category>typescript</category><category>learning in public</category></item></channel></rss>