HomeNotesMembersLogin

Field Notes

Principals

Consciously working to reduce complexity is paramount to any software project, but perhaps even more so when we begin to blur the lines between client and server. Javascript has afforded us this ability, but we must wield the power responsibly.

No Magic

Nothing is abstracted away. This is a pattern for building web clients. There is no library, SDK, framework or anything more than a starting point. You can use it as a reference or a head start on your next project.

Context Injection

Surface wraps both Hono and Tanstack routers with a dead simple injection pattern that makes testing a breeze and the nuances between client and server easier to reason about.

Single Service BFF and Client

Surface is deployed as a NodeJS (or Bun) app container with a Hono backend. Routes are first matched by Hono and the catch all is delegated to Tanstack Router and renders a client that builds and runs a dev server on Vite..

Flexible Routing

Routes are entirely flexible. You don't have to prefix a backend request with
/api
, unless of course you want to. If Hono matches a route, say to
/auth
it will handle it on the server. If it doesn't match, it will delegate to the "view" router. That router is Tanstack's relatively new but full featured and production ready router. This example uses a new but familiar file system based pattern.

Loaders

The loader paradigm follows the render as you fetch pattern. Newer frameworks like Remix, Astro, Modern and more have elected to move this way for good reason. Loaders in our case are provided by Tanstack Router and are run on the server during an initial request, or the client during subsequent route navigation.

Isomorphism

Because we can inject context into our loaders from the server, it's very tempting to make this code isomorphic and get magical. Rather fortuitously, a router update broke our magic in production. So, we pulled back and ensured the loader code is the same on both the client and server. This keeps the renderer service simple.

That said, it would be nice to have a loader server hook method binding that could allow us to return different status codes or even redirect at the server, but this is a discussion for another day.

App State and SSR

Surface lets us define app state such as a user session and pass it between client and server seamlessly.

To do this without manually adding context into our renderer service we create one new abstraction -- a "state module". State modules have a registry the renderer service will read, then pass the data through Tanstack Router's dehydrate and hydrate methods automatically.