Remix.run’s fullstack paradigm

“Specifically, we believe Remix will develop a brand like Tailwind.css: initially polarizing, and immensely popular among its power users.”

At K-Optional Software, we pride ourselves on our arsenal. We love getting our hands on emerging frameworks and libraries whose weird features enable us to solve thorny problems for our clients.

Recently, for example, we’ve adopted Phoenix (Elixir), Svelte (TypeScript), Tailwind (CSS), Alpine.js, and Solidity which have all made certain application features possible or cost-effective.

Remix.run

I saw the Remix.run version 1 release a few weeks ago and couldn’t immediately grok Remix’s value proposition. So I read the docs, watched the intro video, and built a mind-numbingly simple landing page.

I was prepared for another single-page web application (SPA) library with a different philosophy, i.e. Svelte. However, I found Remix.run delivers a new philosophy on full-stack applications entirely. Within a few hours we had identified several long-standing quirks of traditional web development that Remix.run manages to tame.

A short-intro

I recommend looking at the docs and Remix’s example app for effectively picking up Remix.

Remix.run is not a frontend framework but a full-stack framework. You write backend code and frontend code in the same files. This is a stark departure from the trend of the past 5-10 years of separating REST endpoints from user-interface with an iron curtain. It feels sloppy at first– there are reasons why modern software architecture separates these systems. However, with Remix, you get the SPA + API experience for the price of one codebase.

Before Remix

In the spirit of “decoupling”, we write server code as though it were agnostic of its client(s). Except, we sometimes write endpoints specifically for an awkward client need, such as uploading a file or approving a charge (PUT /payments/{id}/approve anyone?). In these cases, decoupling is artificial, and I’d argue unproductive.

Remix abandons the noble server-client decoupling. Like Tailwind.css, sometimes the pendulum strikes gold on its way back.

Take a look for example at code I wrote for Receivable.dev’s contrived wait-list (I omitted some layout JSX). In about 80 sparse lines of code, we dynamically configure HTML meta tags, make a server-side fetch, render a signup-form, validate form submissions on the backend, and interact with the auth-protected Airtable API.

import { MetaFunction, LoaderFunction, Form } from "remix";
import { useLoaderData, json, redirect } from "remix";
import * as EmailValidator from "email-validator";
import { SECRET_AIRTABLE_API_KEY, SECRET_API_KEY, ENDPOINT } from "./config.ts";

// This block runs on the server!
export let loader: LoaderFunction = async () => {
  const data = await fetch(`${ENDPOINT}?apiKey=${SECRET_API_KEY}`).then(
    (resp) => resp.json()
  );
  return json(data);
};

export let meta: MetaFunction = () => {
  return {
    title: "Remix Starter",
    description: "Welcome to remix!",
  };
};

// Automatically glued to your form,
// this block runs on the server. You can use secret keys here
export const action = async ({ request }: any) => {
  const formData = await request.formData();
  const email = formData.get("email");

  if (!EmailValidator.validate(email)) {
    const errors = {
      email: true,
    };
    return errors;
  }
  // I can use my secret Airtable API key
  // as ergonomically as though I were making
  // the request on the client
  await fetch(`https://api.airtable.com/v0/${AIRTABLE_APP_KEY}/Waitlist`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${SECRET_AIRTABLE_API_KEY}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      records: [
        {
          fields: {
            Name: email,
          },
        },
      ],
    }),
  });
  return redirect("/thank-you");
};

// This is a traditional react component that runs on the client
export default function Index() {
  // don't need this data actually, but we could have a free
  // server side request if we wanted
  let data = useLoaderData<IndexData>();

  // server side form validation
  const errors = useActionData();
  return (
    <Form method="post">
      {errors?.title && <em>That email is invalid</em>}
      <input
        required
        name="email"
        className="p-1 w-full text-black"
        type="email"
      ></input>
      <button
        className="bg-gray-800 w-full p-1 text-white hover:bg-gray-600 active:bg-gray-400"
        type="submit"
      >
        {" "}
        Join the waitlist
      </button>
    </Form>
  );
}

Problems solved with Remix

  1. Needing to validate form data on client AND server
  2. Suboptimal, large Ajax payloads due to limitations of REST API abstraction
  3. Existence of “proxy endpoints” so that a client can ultimately interact with an auth-protected 3rd party API.

1. Double form validation

Before Remix

Let’s say you have an SPA that intakes data via a POST request. If you want a reasonable user-experience, you’ll validate the form on the frontend. That might mean ensuring phone numbers are well-formed and making a text-field required under some condition. Built-in browser validation has come a long way and I recommend it whenever possible. For the case I’ve just described, however, you would need to reach for a dedicated tool like Formik, a great library that’s a lot to set up.

By the time the form submission arrives to the backend, we know it must be valid so we can accept it at face value. Just kidding! We need to rerun validation so that someone can’t, for example, use the browser developer tools to bypass the client validation.

Something feels wrong about double validation. A few times we’ve reach past the SPA for Laravel’s Blade and Django Templates, which validate just once since templating occurs on the server for these tools. But often we can’t afford to ditch the SPA; users don’t miss full page reloads and nor do we.

With Remix

With Remix you just validate once in the action function. The code lives in the same file as the form’s jsx, but it executes on the server, beating a client-bypass attempt. Technically, the form payload travels a round trip before feedback appears but in practice, the user experience is virtually the same.

2. Suboptimal Ajax payloads

Before Remix

One page of a non-trivial SPA usually fetches multiple resources. After all, a single modern webpage likely has multiple things going on– like a chat bar, some content, notifications, a user profile etc– and a modularized REST endpoint won’t return multiple resources. As a result, a mature SPA sends lots of traffic over the wire.

GraphQL provides one alternative, enabling multiple-resource fetches. We like GraphQL though sometimes avoid it for one reason or another– rate-limiting complexity and relatively heavy boilerplate to name a few.

With Remix

With Remix, the data that your React component consumes is the only data that travels over the internet. And the best part is that the ergonomics don’t really change; you feel as though you’re simply manipulating more data than you need client-side, but the hidden server-client contour migrates the manipulation to where it’s cheaper.

3. Proxy endpoints

Before Remix

Have you ever wished you could somehow use a server-side service directly from your client JavaScript? For example, it would be cool if you could somehow to charge a credit card via Stripe right from the browser. Unfortunately that’d be a massive security vulnerability considering client code is effectively public. As a result, we find ourselves at K-Optional often creating REST endpoints that essentially proxy such private third party API calls (except with authentication guards etc). In the case of Stripe charges, that might mean creating a “charge” endpoint that merely invokes Stripe’s server library.

With Remix

With Remix, certain code blocks execute on the server only, so you can access sensitive integrations without wiring up dedicated endpoints. Take a look at the example code posted above; that Airtable API code should not run client side because it would mean exposing all possible Airtable API operations to our user, including dropping the database. And it won’t because Remix doesn’t bundle the action block on the client.

Sure, functionally this is like a proxy since Remix packages the server block as an endpoint. But it doesn’t feel like one because Remix does all the work.

Cons of the Remix philosophy

Cloud coupling

We really appreciate that React, Vue, Svelte, Ember, and even Angular build into static files. Every cloud provider offers optimized file-serving services, so continuous-integration pipelines are simple: 1. build 2. synchronize dist with file-server.

Remix builds server and client code, therefore:

  1. Deployment is slightly more complicated.
  2. You can’t throw up your build onto an S3 and just expect it to work.

We noticed that Remix includes build instructions that are cloud specific which takes some getting used to.

Lack of guardrails

With the SPA + REST API paradigm, even poorly architected code will take a familiar shape. The context-boundaries mean you know when you’re on the server and generally your responsibility.

Removing the guardrail between client and server increases the opportunity for programmatic abominations.

Conclusion

Based upon framework patterns we’ve seen over the years, we expect many developers to coalesce around Remix as they adjust to the paradigm. Specifically, we believe Remix will develop a brand like Tailwind.css: initially polarizing, and immensely popular among its power users. After years of incremental developments in web frameworks, we welcome its philosophical shift.