Remix.run is great! But can it have a full backend in under a minute?

Remix.run is great! But can it have a full backend in under a minute?

Remix.run is gaining ground as a powerful and friendly Javascript framework. Read on to explore how to make it even better by provisioning its databases in under a minute.

What is Remix?

Remix is a react-based fullstack web framework that is now living rent free on my twitter feed.

With all this praise, I’ve decided to take it for a spin. I wanted to know more about it, in particular how well it fits with ChiselStrike backends.

My experience was nothing short of great! In particular, I liked the way in which their tutorials are organized. Remix works with many technology stacks, and the tutorials involve popular choices, so that you can develop an entire application just by following the instructions.

Real-life applications have different requirements, and to keep the examples realistic, the Remix tutorial relies on Remix Stacks, each with its own deployment and database choices

The most basic stack is the Indie Stack. According to Remix:

It’s also a perfect, low-complexity bootstrap for MVPs, prototypes, and proof-of-concepts that can later be updated to the Blues stack easily.

The Indie Stack ships with a SQLite database, and you can do everything you need locally. The blues and grunge stack are production-ready stacks, with more powerful databases and more complex deployment models.

What is ChiselStrike?

ChiselStrike is a Backend-as-a-Service focused on TypeScript users. It includes everything from the data layer up to the runtime, and focuses on abstracting the database details, allowing users to seamlessly move from prototype to production as project requirements evolve.

ChiselStrike includes a tools for local development, and a deployment model that pulls from your GitHub project (in beta). It can be used with any frontend, as well as headless, for your microservices architecture.

In this article, I’ll implement Remix’ blog tutorial using ChiselStrike, instead of the combination of Prisma+SQLite. I’ll show how using ChiselStrike, deployments are easy. It’s as if the Indie and Blues stack came together in one!

Only the tutorial content (adding blog posts) is implemented, and the rest of the project that is using Prisma+SQLite is left as-is. You are welcome to port the rest to using ChiselStrike if you want, but remember: as developers, we should resist the urge to re-implement everything! Whenever possible, it’s a big win to incrementally add existing technology stacks in existing projects instead of rewriting the world.

Getting Started

I’ll now walk you through the process of using ChiselStrike to implement the blog post example. For those of you who want to jump ahead to end result, you can check out ChiselStrike’s example repository.

The git history for that repository shows two important commits: The firstinitializes the remix project, and should get you to the same end result as if you were following the preparation steps in the Remix official tutorial. The second adds ChiselStrike code, and is what we will be discussing here.

Starting from the baseline Remix project, we need to add the ChiselStrike models and endpoints. To do that, we’ll initialize a ChiselStrike project inside the Remix directory. This will create a directory called chiselstrike using our version 0.11.1:

npx -c v0.11.1 create-chiselstrike-app chiselstrike

We want to start the ChiselStrike development service together with Remix’, so we will add the following line to the scripts section in the top-level remix package.json:

"dev:chiselstrike": "cd chiselstrike; npm run dev",

This will essentially go into the chiselstrike directory that we just created, and run ChiselStrike’s dev server command — you can also do that manually if you want to.

Now we can install the node modules that remix needs, and provide a .env file. For this example, we’ll just use the .env.examplefile that Remix ships, but you can add any value here that you want:

$ npm install # install remix' npm packages
$ cp .env.example .env # uses the example environment variables, good enough for testing
$ npm run dev # runs the Remix development server, and also starts ChiselStrike

After doing that, you should have your Remix development server running on port 3000, and ChiselStrike’s running on port 8080.

We’ll add an entity for a blog post in chiselstrike/models/Post.ts :


import { ChiselEntity } from "@chiselstrike/api";

export class Post extends ChiselEntity {
  title: string;
  markdown: string;
}

Note that the entity definition happens purely in TypeScript, and there is no need to translate from any other representation, issue database migrations, or etc.

ChiselStrike embeds both a TypeScript runtime and a data access layer. So in the endpoints directory, we can write any custom business logic we want using these models. But we’ll start simple by adding basic CRUD functionality. We do that by adding a file called chiselstrike/endpoints/posts.ts:

import { Post } from "../models/Post.ts";
export default Post.crud();

And that’s it! That’s all it takes to have your data layer working for a very simple CRUD application. Because that’s already a complete set of CRUD endpoints, we can use that directly to seed the database.

We’ll use curl here (because we like software that runs on Mars) but you could use anything that allows you to issue POST requests:

$ curl -d '{"title": "my first post", "markdown": "Here is our first post"}' http://localhost:8080/dev/posts

{
  "id": "19a43957-ef7f-4ebd-8096-2d7e37494e2a",
  "title": "my first post",
  "markdown": "Here is our first post"
}

$ curl -d '{"title": "my second post", "markdown": "Here is our second post"}' http://localhost:8080/dev/posts
{
  "id": "c2730807-5cef-4440-a796-ec62167126dc",
  "title": "my second post",
  "markdown": "Here is our second post"
}

We can now test to see if everything was seeded correctly.


$ curl http://localhost:8080/dev/posts
{
  "next_page": "http://localhost:8080/dev/posts?cursor=eyJheGVzIjpbeyJrZXkiOnsiZmllbGROYW1lIjoiaWQiLCJhc2NlbmRpbmciOnRydWV9LCJ2YWx1ZSI6ImMyNzMwODA3LTVjZWYtNDQ0MC1hNzk2LWVjNjIxNjcxMjZkYyJ9XSwiZm9yd2FyZCI6dHJ1ZSwiaW5jbHVzaXZlIjpmYWxzZX0%3D",
  "prev_page": "http://localhost:8080/dev/posts?cursor=eyJheGVzIjpbeyJrZXkiOnsiZmllbGROYW1lIjoiaWQiLCJhc2NlbmRpbmciOnRydWV9LCJ2YWx1ZSI6IjE5YTQzOTU3LWVmN2YtNGViZC04MDk2LTJkN2UzNzQ5NGUyYSJ9XSwiZm9yd2FyZCI6ZmFsc2UsImluY2x1c2l2ZSI6ZmFsc2V9",
  "results": [
    {
      "id": "19a43957-ef7f-4ebd-8096-2d7e37494e2a",
      "title": "my first post",
      "markdown": "Here iss our first post"
    },
    {
      "id": "c2730807-5cef-4440-a796-ec62167126dc",
      "title": "my second post",
      "markdown": "Here is our second post"
    }
  ]
}

We can now call the ChiselStrike endpoints from Remix. We can add those calls anywhere, even directly into the routes directory, but to keep things well organized we’ll follow the same convention as the blog tutorial has, and add an app/models/post.server.ts file where all things ChiselStrike will live. Notice how we can even consume the models directly, as they are pure TypeScript, and there’s no need to translate from the data layer models to TypeScript.

You can call the CRUD endpoints through any library you want. In this example we’re just using fetch , to make everything explicit. But you could use Axios or any other library that you prefer.

import type { Post } from "../../chiselstrike/Post";
export type { Post } from "../../chiselstrike/Post";

function chiselUrl(name: string): string {
  // Use environment variables to figure out where we are. Feel free to add this
  // to your .env, but to keep things simple we'll add a default
  const chiselServer = process.env.CHISEL_SERVER ?? "http://localhost:8080";
  // Which version? Versions are isolated backends that can be used in the same
  // chisel instance. You can do test databases, API versions, or what your heart
  // desires. By (local) default this is "dev", but when deployed it is customary
  // for this to be "main" (github's main branch) or a PR number.
  const chiselVersion = process.env.CHISEL_VERSION ?? "dev";
  return `${chiselServer}/${chiselVersion}/${name}`;
}

// In practice you could use a higher level library here such as Axios, or
// even hide everything behind tRPC. We are open coding fetches so it becomes
// abundandtly obvious that those are really just HTTP endpoints!
//
// You can do anything you want with them, including going headless, microservices, etc.

// The first function just gets all posts. No pagination, no filtering, though all of that
// can be easily added!
export async function getPosts(): Promise<Array<Post>> {
  const url = chiselUrl("posts");
  console.log(url);
  return fetch(url).then((response) => {
    return response.json().then((crud) => {
      return crud.results; // See how the example curl command returned: the actual data is on the `results` property
    });
  });
}

// The second function will just call the POST endpoint and create an object.
export async function createPost(post) {
  const url = chiselUrl("posts");
  return fetch(url, {
    method: "post",
    body: JSON.stringify(post),
    headers: { "Content-Type": "application/json" },
  });
}

// Last option is to get a post, given its ID. With this, we have everything we need!
export async function getPost(id: string): Promise<Post> {
  const url = `${chiselUrl("posts")}/${id}`;
  return fetch(url).then((response) => {
    return response.json();
  });
}

With those calls, we implemented the same functions that Remix used in the blog tutorial: getPost, getPosts, and createPost. Notice that once we know what the URL is, all we have to do is call the REST endpoints.

The example has to be modified slightly to account for the fact that there is no need for a slug, since an id was created automatically (but you could ultimately add one if you wanted). Other than that, it is by and large very similar to what you would have coded by following Remix’ tutorial.

And how does that work? Let’s open the browser and point to localhost:3030and see:

🎉 Ta-da! 🎉

One stack to rule them all?

Now that it all worked locally, how can we go from prototype-to-production with ChiselStrike?

First, we’ll login to the ChiselStrike public Beta on the website:

You will authorize a list of repositories to work with ChiselStrike. In this case, I forked the chiselstrike-examples repository and it is now visible in the project list.

In the next step, we can specify which branch to deploy from, and the subdirectory you can find the ChiselStrike code. In our case, we’ll deploy from the main branch, and the code lives at remix/chiselstrike

We’ll not have to make any changes to our Remix app for your data layer to go to production. All you have to do is edit your .env variable, pointing to the domain for the project, and the branch. This information will be available in your project page:

In your .env file, we’ll add the following two variables:

CHISEL_SERVER="https://chiselstrike-examples-glommer.chiselstrike.io"
CHISEL_VERSION="main"

And that’s it! Now if you refresh your local website at localhost:3000, you will see that everything keeps working, except the list of posts is empty. That’s because the fresh deployment has a fresh database. But we can add a new post through the admin interface:

This is now running the deployed version of ChiselStrike. From prototype to production, by changing a couple of environment variables.

Now what?

Although we deployed ChiselStrike to the platform, in a full production experience, you obviously have to deploy the frontend somewhere as well. You can use any platform you want: Fly.io as the original Indie Stack does, but also Vercel, or Netlify.

Your production application would also not allow POSTs from anyone, so adding authentication is a good second step. We’ll cover that in a later article.

ChiselStrike is Open Source, and you can find us on Github. You’re also very welcome to discuss this and other examples on our Discord. Happy chiseling!