If you read my previous post about Remix, you know that I'm excited about it. As much as I love NextJS, Remix introduces a new paradigm that combines the best parts of server- and client-side rendering while removing complexity.
[Insert memes about new JavaScript frameworks here...]
JavaScript fatigue aside, I did have a complexity problem with my side project Trad Archive.
I had chosen a stack which enabled serverless infrastructure, granular data access, client-side caching, and full preview deploys:
Deployed via CI/CD on every git push:
The benefits of that stack are nice, but it has a lot of dependencies. If Trad Archive is going to be a sustainable open source project, its code needs to be easily understandable for other devs. There can't be obscure edge cases and quirks.
This illustrates the gist of what was happening (h/t to Theo):
It was time to simplify.
I wanted to keep the benefits of the old stack – especially the full preview deploys – but wanted to remove boilerplate, reduce the number of network interfaces, have fewer dependencies, and streamline deployment.
Remix turned out to be a great solution.
The new stack is:
Deployed via CI/CD to:
The basic summary is that instead of having a client app which consumes data from a separate backend API, the client and backend get colocated into the same Remix app.
I recommend reading through the Remix homepage to get a better sense of how it works.
Basically, when a request comes into your Remix app, you can define a loader function which runs on the server, fetches data, does an initial render of your component, and returns static HTML to the client. Then the client is progressively enhanced with JS so you still have a fully dynamic app.
The loader
is essentially the API and runs behind the scenes as a serverless function. But Remix handles abstracting away the infrastructure. The data fetching code sits in the same file as the markup. To the developer, it's seamless, and there is no third-party network request.
// MyComponent.tsx
export async function loader() {
const items = await db.item.findMany();
return json(items);
}
export default function MyComponent() {
const items = useLoaderData();
return (
<div>
{items.map((i) => (
<p>{i.name}</p>
))}
</div>
);
}
This removes a huge amount of boilerplate code. No more if (data.loading) { return <Loading /> }
. No more defining GraphQL fragments in your React app. No more normalized data caching via Apollo Client -- which, incidentally, I recommend avoiding at all costs.
After I migrated to Remix, overall app code decreased from about 30k to 20k lines.
And things were a lot simpler to reason about. If you can understand Remix and Prisma, that's all you really need to know.
There's a single deployment via Vercel, instead of simultaneous deployments to Vercel and AWS Lambda. That makes production and preview deploys simpler, as well as rollbacks.
I also took the opportunity to remove features that weren't being actively used.
Deleting code is one of my favorite things in software engineering. It usually means you have learned something and found a better approach.
"The fastest code is the code which does not run. The code easiest to maintain is the code that was never written." – Robert Galanakis
Now Trad Archive is easier to develop on, faster for users, and better positioned for a sustainable future.