An Updated Tech Stack: Remix

I recently changed this website's tech stack to Remix and it's worth explaining why.

The last time I wrote about my go-to tech stack (which this site was utilising) was way back in 2016. In that post I had just migrated to AWS for better performance than Digital Ocean, and was using an EC2 host and RDS database.

Building costs

That was all-fine-and-good while I was receiving AWS credits for writing some Alexa skills, but around 2 years ago I stopped receiving those credits - and those RDS costs really started to bite. I was paying something in the range of £15/mo total if I used On-Demand pricing, and around £9/mo if I paid for a year up-front. That kind of pricing for a "self-hosted" blog website kind of sucks - I'd be better off migrating to Squarespace at that point (although would drop some of the tech-y fun side of maintaining a personal website).

Earlier this year I decided I had had enough of paying those costs and started looking around for alternative options. I was also relatively keen to change the tech stack that the site utilised - Python/Django may have been my first introduction to web development, but I hadn't used it professionally in over 7 years.

During my research, I settled on Remix which looked like an attractive proposition to learn something more "cutting edge", as well as migrating to a fully Typescript and React-based website (as opposed to a mix of Python and HTML templating), which is a stack I've worked with since 2017.

Remix

Remix is a "full stack" framework as it is somewhat opinionated about how you write your back-end code and your front-end code - and in fact Remix encourages you to mix your back-end code and front-end code in the same file.

Taking the Remix website's example code, I'll label which parts execute on the server-side and which parts execute on the client-side:

// Runs on the server-side. Must be called "loader"
export async function loader({ request }) {
  return getProjects();
}

// Runs on the server-side. Must be called "action".
export async function action({ request }) {
  const form = await request.formData();
  return createProject({ title: form.get("title") });
}

// Runs on both server and client-side. Name doesn't matter.
export default function Projects() {
  const projects = useLoaderData();
  const { state } = useNavigation();
  const busy = state === "submitting";

  // Basically just regular React at this point
  return (
    <div>
      {projects.map((project) => (
        <Link to={project.slug}>{project.title}</Link>
      ))}

      <Form method="post">
        <input name="title" />
        <button type="submit" disabled={busy}>
          {busy ? "Creating..." : "Create New Project"}
        </button>
      </Form>
    </div>
  );
}

The above file describes a "route" which generally maps well to the concept of a page of a website. Breaking down what's visible in this route's file:

  • The loader describes what data needs needs to be fetched when the page is loaded, so this is where you would do your database calls.
  • The action describes what happens when a form is submitted, so also likely involves some database calls.
  • The default exported function describes what should be rendered on the route. This will be run on the server as a server-side render, and then re-run on the client side as part of a hydration step, and potentially run several more times if any client-side only interaction is needed (just like regular React components).

Remix also aims to make use of what the browser gives us out of the box, so form submissions are generally making use of browser-based HTML form submissions rather than AJAX calls (as one example). This has the added benefit that Remix websites are written fully in React, but can still generally work well even if JavaScript is disabled in the browser.

Remix has lots more smarts, but the remix.run website will showcase them better than I can. It's well worth checking out.

Hosting

So, my plan was to rewrite my Django website in Remix to improve my developer experience - but that alone doesn't solve the EC2 + RDS costs.

Remix has partnered with fly.io for many of their examples (including the starter project I began with), and fly.io has a fairly generous free tier (up to three 1-shared-cpu + 256MB RAM instances). So I was happy to give fly.io a go for initial hosting. In the future something like AWS Lambda could be an attractive solution to improve performance while keeping costs low.

I also needed some kind of persistent storage for the blog posts themselves, ideally with a WYSIWYG editor for writing these posts. After evaluating a few options, Sanity.io stood out as a good balance between an out-of-the-box solution, good developer experience, and a generous free tier (effectively summarised as 1M requests/mo and 100GB storage).

A few evenings of combining all these together, along with Tailwind and DaisyUI components, and my site was migrated - which much improved developer experience and zero monthly cost. Success!

I've also written 2 other Remix sites at this point: lilyjmakes.com for my wife, and evolutionracing.academy for my online racing community. All three I consider "work in progress", especially with regards to styling. I've not yet devoted enough time to styling (and styling with Tailwind in particular) to really say any of the three look beautiful. Perhaps a task for this year!

If you enjoyed this post, please check out my other blog posts.