How to host a Remix application?

Back4app Containers Remix Cover

In this article, you’ll learn everything you need to know to get started with Remix. We’ll look at its pros and cons, demonstrate how to build a Remix app, and lastly, how to deploy Remix.

What is Remix?

Remix is a modern full-stack web framework for building fast, slick, and resilient apps. It leverages the power of server-side rendering (SSR), enabling faster load times and improved SEO for dynamic web applications.

With Remix, developers can seamlessly integrate client-side and server-side logic within a unified codebase. Compared to other popular React frameworks, Remix does a lot of things differently. That includes:

  1. File-based routing (a file in the routes directory represents a specific route)
  2. Traditional form handling (using HTML and HTTP requests, similar to PHP)
  3. State management (state is only stored on the server)
  4. Data loading (decoupled from the components)

Remix was created in 2020 and was initially available under a yearly license fee. Later, in October 2021, the Remix team decided to open-source the project. It is now available under the MIT license.

End of 2022, Remix was acquired by Shopify for 2.1 billion dollars.

Benefits of Remix

Let’s look at the main benefits of using the Remix framework.

File-based Navigation

Remix is built on top of React Router, a powerful client-side routing solution. In fact, Remix was created by the same developer team that created React Router.

The framework utilizes a file-based navigation system, which simplifies the code organization. It allows developers to associate routes, components, and resources with specific files or directories.

Here’s an example:

app/
└── routes/
    ├── $noteId.tsx              // matches: /<noteId>/
    ├── $noteId_.destroy.tsx     // matches: /<noteId>/destroy
    ├── $noteId_.edit.tsx        // matches: /<noteId>/edit
    ├── _index.tsx               // matches: /
    └── create.tsx               // matches: /create

Best of all, it supports nested routing via <Outlet/>. This results in shorter loading times, better error handling, and more!

Server-Side Rendering (SSR)

Remix leverages the power of server-side rendering.

In vanilla React apps, the data is usually fetched on the client side and then injected into the DOM. This is known as client-side rendering (CSR).

Remix, on the other hand, takes a different approach. It first fetches the data on the backend, renders the HTML with the fetched data, and then serves that to the client.

Server-side rendering tends to result in better performance and more SEO-friendly apps.

Form Handling

Remix takes form handling back to the basics.

Instead of using a bunch of controlled components and JavaScript, it uses traditional HTML forms and HTTP requests.

When a form is submitted, it sends an HTTP request to a specific route, which is then processed on the server side using the action() function. It works similarly to good old PHP.

This means that Remix requires absolutely no JavaScript to process forms. While this is great, it might make form validation and error displaying a bit trickier.

State Management

In this context, state refers to synchronizing the server and client data.

Remix makes state management a breeze. It eliminates the need for Redux, Zustand, React Query, Apollo, or any other client-side state management library.

When using Remix all the state is handled by the server. The client holds virtually no state; therefore, the synchronization process is not required.

Data from server to client is passed through various functions such as loader() and action().

Transitions and Optimistic UI

Remix transitions make moving between pages smooth and fast by loading data in advance and using animations.

Optimistic UI instantly reflects user actions, making websites feel more responsive by showing changes before they’re confirmed.

The transitions and optimistic UI greatly enhance the user experience by reducing perceived latency and providing immediate feedback.

Limitations of Remix

Even though Remix is great, it comes with some limitations.

Complex

Remix isn’t the easiest framework to use, and its documentation (at the time of writing) isn’t the best.

The framework also does a lot of things differently from vanilla React. If you’re a React developer, it might take you some time to wrap your head around all the different Remix concepts.

Less Popular

Remix is still a relatively new framework. Its public release only dates back to October 2021. It is less mature and battle-tested compared to some competitors, like Next.js.

Looking at the GitHub stars, we can see that Remix (27k stars) is way less popular than Next.js (120k stars). At the time of writing, Remix hasn’t been adopted by any tech giants except for Shopify.

Tightly Coupled

Remix apps have a tightly coupled frontend and backend. While this approach works for smaller projects, some developers prefer the flexibility of a separate frontend and backend.

Additionally, separating the frontend from the backend can make your code more maintainable and easier to test.

No SSG or ISR

Remix doesn’t support static site generation (SSG) or incremental static regeneration (ISR).

How to deploy a Remix application?

In this section, we’ll build and deploy a Remix application.

Prerequisites

To follow along, you will need:

  • Basic understanding of TypeScript
  • Experience with Docker (and containerization technology)
  • Node.js and a JavaScript IDE installed on your machine
  • Docker Desktop installed on your machine

Project Overview

Throughout the article, we will be working on a notes web app. The web app will allow users to create, retrieve, edit, and delete notes.

For the backend, we’ll utilize Back4app BaaS, and for the frontend, we’ll use the Remix framework. Once the frontend is coded, we’ll deploy it to Back4app Containers.

The final product will look something like this:

Back4app Remix Notes

I suggest you first follow along with the notes web app. After the article, you should be able to deploy your own Remix applications.

Backend

In this article section, we’ll use Back4app to build our app’s backend.

Create App

Start by logging into your Back4app account (or creating one if you still need to get it).

As you sign in, you’ll be redirected to the “My Apps” portal. To create a backend you first need to create a Back4app app. To do that, click “Build new app”.

Back4app Apps Dashboard

Back4app provides two solutions:

  1. Backend as a Service (BaaS) — a fully-fledged backend solution
  2. Containers as a Service (CaaS) — Docker-based container orchestration platform

Since we’re working on a backend, select “Backend as a Service”.

Back4app BaaS Create

Name your app, leave everything else as default, and click “Create”.

Back4app Create App Details

The platform will take a while to setup everything required for your backend. That includes the database, application interface, scaling, security, etc.

Once your app is ready, you’ll be redirected to the app’s real-time database view.

Back4app Database View

Database

Moving along, let’s take care of the database.

Since we’re building a relatively simple app, we’ll only need one model — let’s name it Note. To create it, click the “Create a class” button at the top left of the screen.

Name it Note, enable “Public Read and Write Enabled”, and click “Create class & add columns”.

Back4app Database Class Create

Then, add the following columns:

+-----------+--------------+----------------+----------+
| Data type | Name         | Default value  | Required |
+-----------+--------------+----------------+----------+
| String    | emoji        | <leave blank>  | yes      |
+-----------+--------------+----------------+----------+
| String    | title        | <leave blank>  | yes      |
+-----------+--------------+----------------+----------+
| File      | content      | <leave blank>  | no       |
+-----------+--------------+----------------+----------+

Once you’ve created the class, populate the database with sample data. Create a few notes by providing the emojis, titles, and content. Alternatively, import this database export.

Back4app Database Populated

Great, that’s it!

We’ve successfully created a backend without writing any code.

To learn more about Backend as a Service — check out What is Backend as a Service?

Frontend

In this article section, we’ll build our app’s frontend using the Remix framework.

create-remix

The easiest way to bootstrap a Remix project is via the create-remix utility. This tool creates a production-ready Remix project.

It takes care of the directory structure and dependencies, sets up the bundler, etc.

Create a new Remix project by running the following command:

$ npx create-remix@latest remix-notes

Initialize a new git repository? Yes
Install dependencies with npm?   Yes

If you’ve never used create-remix, it’ll be installed automatically.

Once the project has been created, change your active directory to it:

$ cd remix-notes

Lastly, start the development server:

$ npm run dev

Open your favorite web browser and navigate to http://localhost:5173. The default Remix landing page should pop up on your screen.

TailwindCSS

To make our lives a little bit easier, we’ll use TailwindCSS. TailwindCSS is a utility-first framework that allows you to rapidly build frontends without writing any plain CSS.

First, install it using npm:

$ npm install -D tailwindcss postcss autoprefixer

Next, run tailwindcss init:

$ npx tailwindcss init --ts -p

This will configure your project and create a tailwind.config.ts file in the project root.

Modify its content property like so to let Tailwind know in what files utility classes will be used:

// tailwind.config.ts

import type {Config} from "tailwindcss";

export default {
  content: ["./app/**/*.{js,jsx,ts,tsx}"],  // new
  theme: {
    extend: {},
  },
  plugins: [],
} satisfies Config;

Create a new file named tailwind.css in the app directory with the following contents:

/* app/tailwind.css */

@tailwind base;
@tailwind components;
@tailwind utilities;

Lastly, import it in root.tsx via links:

// app/root.tsx

// other imports
import {LinksFunction} from "@remix-run/node";
import stylesheet from "~/tailwind.css?url";

export const links: LinksFunction = () => [
  { rel: "stylesheet", href: stylesheet },
];

// ...

That’s it!

We’ve successfully installed TailwindCSS.

Routes

Our web app will have the following endpoints:

  1. / displays all the notes
  2. /create allows users to create notes
  3. /<noteId> displays a specific note
  4. /<noteId>/delete allows users to delete a specific note

To define these routes, create the following directory structure in the app folder:

app/
└── routes/
    ├── $noteId.tsx
    ├── $noteId_.destroy.tsx
    ├── $noteId_.edit.tsx
    ├── _index.tsx
    └── create.tsx

As you might’ve guessed, the prefix $ is used for dynamic parameters, and . is used instead of /.

Views

Moving along, let’s implement the views.

To make our code more type-safe, we’ll create an interface named Note that resembles our Note database class.

Create a folder named store, and within it, create NoteModel.ts with the following contents:

// app/store/NoteModel.ts

export default interface NoteModel {
  objectId: string;
  emoji: string;
  title: string;
  content: string;
  createdAt: Date;
  updatedAt: Date;
}

Then paste in the view code for _index.tsx, $noteId.tsx, and create.tsx:

// app/routes/_index.tsx

import {Link, NavLink} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";

const notes = [
  {objectId: "1", emoji: "📝", title: "My First Note"},
  {objectId: "2", emoji: "📓", title: "My Second Note"},
  {objectId: "3", emoji: "📔", title: "My Third Note"},
] as NoteModel[];

export default function Index() {
  return (
        <>
      <Link to={`/create`}>
        <div className="bg-blue-500 hover:bg-blue-600 text-lg font-semibold text-white
         px-4 py-3 mb-2 border-2 border-blue-600 rounded-md"
        >
          + Create
        </div>
      </Link>
      {notes.map(note => (
        <NavLink key={note.objectId} to={`/${note.objectId}`}>
          <div className="hover:bg-slate-200 text-lg font-semibold
            px-4 py-3 mb-2 border-2 border-slate-300 rounded-md"
          >
            {note.emoji} {note.title}
          </div>
        </NavLink>
      ))}
    </>
  );
}
// app/routes/$noteId.tsx

import {Form} from "@remix-run/react";
import NoteModel from "~/store/NoteModel";

const note = {
  objectId: "1", emoji: "📝", title: "My First Note", content: "Content here.",
  createdAt: new Date(), updatedAt: new Date(),
} as NoteModel;

export default function NoteDetails() {
  return (
    <>
      <div className="mb-4">
        <p className="text-6xl">{note.emoji}</p>
      </div>
      <div className="mb-4">
        <h2 className="font-semibold text-2xl">{note.title}</h2>
        <p>{note.content}</p>
      </div>
      <div className="space-x-2">
        <Form
          method="post" action="destroy"
          onSubmit={(event) => event.preventDefault()}
          className="inline-block"
        >
          <button
            type="submit"
            className="bg-red-500 hover:bg-red-600 font-semibold text-white
              p-2 border-2 border-red-600 rounded-md"
          >
            Delete
          </button>
        </Form>
      </div>
    </>
  );
}
// app/routes/create.tsx

import {Form} from "@remix-run/react";

export default function NoteCreate() {
  return (
    <>
      <div className="mb-4">
        <h2 className="font-semibold text-2xl">Create Note</h2>
      </div>
      <Form method="post" className="space-y-4">
        <div>
          <label htmlFor="emoji" className="block">Emoji</label>
          <input
            type="text" id="emoji" name="emoji"
            className="w-full border-2 border-slate-300 p-2 rounded"
          />
        </div>
        <div>
          <label htmlFor="title" className="block">Title</label>
          <input
            type="text" id="title" name="title"
            className="w-full border-2 border-slate-300 p-2 rounded"
          />
        </div>
        <div>
          <label htmlFor="content" className="block">Content</label>
          <textarea
            id="content" name="content"
            className="w-full border-2 border-slate-300 p-2 rounded"
          />
        </div>
        <div>
          <button
            type="submit"
            className="bg-blue-500 hover:bg-blue-600 font-semibold
              text-white p-2 border-2 border-blue-600 rounded-md"
          >
            Create
          </button>
        </div>
      </Form>
    </>
  );
}

There’s nothing fancy going on in this code. All we did was use JSX in combination with TailwindCSS to create the user interface.

As you might’ve noticed, all the components are uncontrolled (we’re not using useState()). On top of that, we’re using an actual HTML form.

This is because the Remix framework handles forms similar to PHP using HTTP requests, unlike React.

Parse

There are multiple ways to connect to your Back4app-based backend. You can use:

  1. RESTful API
  2. GraphQL API
  3. Parse SDK

The easiest and the most robust way is certainly Parse SDK. Parse SDK is a software development kit that provides a number of utility classes and methods for easily querying and manipulating your data.

Start by installing Parse via npm:

$ npm install -i parse @types/parse

Create a .env file in the project root like so:

# .env

PARSE_APPLICATION_ID=<your_parse_app_id>
PARSE_JAVASCRIPT_KEY=<your_parse_js_key>

Ensure to replace <your_parse_app_id> and <your_parse_js_key> with actual credentials. To obtain the credentials, navigate to your app and select “App Settings > Server & Security” on the sidebar.

Then, initialize Parse at the top of root.tsx file like so:

// app/root.tsx

// other imports
import Parse from "parse/node";

const PARSE_APPLICATION_ID = process.env.PARSE_APPLICATION_ID || "";
const PARSE_HOST_URL = "https://parseapi.back4app.com/";
const PARSE_JAVASCRIPT_KEY = process.env.PARSE_JAVASCRIPT_KEY || "";
Parse.initialize(PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY);
Parse.serverURL = PARSE_HOST_URL;

We’ll create a separate file named api/backend.ts to keep our views clean of the backend communication logic.

Create api/backend.ts with the following contents:

// app/api/backend.ts

import Parse from "parse/node";
import NoteModel from "~/store/NoteModel";

export const serializeNote = (note: Parse.Object<Parse.Attributes>): NoteModel => {
  return {
    objectId: note.id,
    emoji: note.get("emoji"),
    title: note.get("title"),
    content: note.get("content"),
    createdAt: new Date(note.get("createdAt")),
    updatedAt: new Date(note.get("updatedAt")),
  };
}

export const getNotes = async (): Promise<NoteModel[]> => {
  // ...
}

export const getNote = async (objectId: string): Promise<NoteModel | null> => {
  // ...
}

// Grab the entire file from: 
// https://github.com/duplxey/back4app-containers-remix/blob/master/app/api/backend.ts

Lastly, modify the views to fetch and manipulate the backend’s data:

// app/routes/index.tsx

import {json} from "@remix-run/node";
import {Link, NavLink, useLoaderData} from "@remix-run/react";
import {getNotes} from "~/api/backend";

export const loader = async () => {
  const notes = await getNotes();
  return json({notes});
};

export default function Index() {
  const {notes} = useLoaderData<typeof loader>();
  return (
    // ...
  );
}
// app/routes/$noteId.tsx

import {getNote} from "~/api/backend";
import {json, LoaderFunctionArgs} from "@remix-run/node";
import {Form, useLoaderData} from "@remix-run/react";
import {invariant} from "@remix-run/router/history";

export const loader = async ({params}: LoaderFunctionArgs) => {
  invariant(params.noteId, "Missing noteId param");
  const note = await getNote(params.noteId);
  if (note == null) throw new Response("Not Found", {status: 404});
  return json({note});
};

export default function NoteDetails() {
  const {note} = useLoaderData<typeof loader>();
  return (
    // ...
  );
}
// app/routes/create.tsx

import {ActionFunctionArgs, redirect} from "@remix-run/node";
import {Form} from "@remix-run/react";
import {createNote} from "~/api/backend";

export const action = async ({request}: ActionFunctionArgs) => {
  const formData = await request.formData();
  const {emoji, title, content} = Object.fromEntries(formData) 
                                    as Record<string, string>;
  const note = await createNote(emoji, title, content);
  return redirect(`/${note?.objectId}`);
};

export default function NoteCreate() {
  return (
    // ...
  );
}
// app/routes/$noteId_.destroy.tsx

import type {ActionFunctionArgs} from "@remix-run/node";
import {redirect} from "@remix-run/node";
import {invariant} from "@remix-run/router/history";
import {deleteNote} from "~/api/backend";

export const action = async ({params}: ActionFunctionArgs) => {
  invariant(params.noteId, "Missing noteId param");
  await deleteNote(params.noteId);
  return redirect(`/`);
};

Code rundown:

  • We used Remix loader() function to load the data. Once the data has been loaded, we’ve passed it to the view as JSON via the json() function.
  • We used the Remix action() function to handle the form submission (e.g., POST).
  • The noteId was passed to views as a parameter.

The app should now be fully working and synced with the Back4app backend. Ensure everything works by creating a few notes, editing them, and then deleting them.

Dockerize App

In this section, we’ll dockerize our Remix frontend.

Dockerfile

A Dockerfile is a plain text file that outlines the steps the Docker engine has to perform to construct an image.

These steps encompass setting the working directory, specifying the base image, transferring files, executing commands, and more.

The instructions are typically depicted in all uppercase and immediately followed by their respective arguments.

To learn more about all the instructions, check out the Dockerfile reference.

Create a Dockerfile in the project root with the following contents:

# Dockerfile

FROM node:20

WORKDIR /app

COPY package*.json ./
RUN npm ci

COPY . .
RUN npm run build

EXPOSE 3000

CMD ["npm", "run", "start"]

This Dockerfile is based on the node:20 image. It first sets the working directory, copies over the package.json, and installs the dependencies.

After that, it builds the project, exposes port 3000, and serves the app.

.dockerignore

When working with Docker, you’ll usually strive to build images that are as small as possible.

Since our project contains certain files that don’t need to be in the image (e.g., .git, build, IDE settings), we’ll exclude them. To do that, we’ll create a .dockerignore file that works similarly to a .gitignore file.

Create a .dockerignore file in the project root:

# .dockerignore

.idea/
.cache/
build/
node_modules/

Adapt the .dockerignore according to your project’s needs.

Build and Test

Before pushing a Docker image to the cloud, it’s a good idea to test it locally.

First, build the image:

$ docker build -t remix-notes:1.0 .

Next, create a container using the newly created image:

$ docker run -it -p 3000:3000
    -e PARSE_APPLICATION_ID=<your_parse_app_id> 
    -e PARSE_JAVASCRIPT_KEY=<your_parse_javascript_key> 
    -d remix-notes:1.0

Ensure to replace <your_parse_app_id> and <your_parse_javascript_key> with the actual credentials.

Your app should now be accessible at http://localhost:3000. It should behave the same way as before the dockerization process.

Push to GitHub

To deploy an app to Back4app Containers, you must first push your source code to GitHub. To do that, you can follow these steps:

  1. Log into your GitHub account (or sign up).
  2. Create a new GitHub repository.
  3. Navigate to your local project and initialize it: git init
  4. Add all the code to the version control system: git add .
  5. Add the remote origin via git remote add origin <remote_url>
  6. Commit all the code via git commit -m "initial commit"
  7. Push the code to GitHub git push origin master

Deploy App

In this last section, we’ll deploy the frontend to Back4app Containers.

Log into your Back4app account and click “Build new app” to initialize the app creation process.

Back4app Create App

Since we’re now deploying a containerized app, select “Containers as a Service”.

Back4app Containers as a Service

Next, you must link your GitHub account with Back4app and import the repository you have previously created. Once connected, select the repo.

Back4app Repository Select

Back4app Containers allows for advanced configuration. Nevertheless, for our simple app, the following settings will suffice:

  1. App Name: remix-notes (or pick your name)
  2. Environment Variables: PARSE_APPLICATION_ID, PARSE_JAVASCRIPT_KEY

Use the values you used in the .env file for the environment variables.

Once you’re done configuring the deployment, click “Deploy”.

Back4app Configure Environment

Wait for a few moments for the deployment to complete. Once deployed, click on the green link on the left side of the screen to open the app in your browser.

For a detailed tutorial, please check Back4app’s Container Remix Documentation.

That’s it! Your app is now successfully deployed and accessible through the provided link. Additionally, Back4app has issued a free SSL certificate for your app.

Conclusion

Despite being a relatively new framework, Remix allows developers to build powerful full-stack web applications.

The framework addresses many complexities of web applications, such as form handling, state management, and more.

In this article, you’ve learned how to build and deploy a Remix application. You should now be able to utilize Back4app to create a simple backend and Back4app Containers to deploy your containerized applications.

The project source code is available on back4app-containers-remix repo.


Leave a reply

Your email address will not be published.