Building Dynamic Web Applications with Remix.js, Drizzle ORM and Tailwind
Tags: typescript, node, frontend, react
Published: 2023-05-02 18:11:33

## What you will learn

I hope this article helps you create a server-side rendered application using Remix. In addition to giving some tips on how to analyze the routes to have a safer approach, handle forms more easily and make a CRUD in a database.

![end result](https://res.cloudinary.com/dj5iihhqv/image/upload/v1683050995/2023-05-02_at_19.08.01_-_Scarlet_Lion-min_rmrl7g.gif)

## What does this article cover

We will cover several aspects, including:

- Tailwind Configuration
- Configure a type safe helper for safe route usage
- Form validation
- Data modeling
- Create, Read, Update and Delete (CRUD) operations

## Prerequisites

Before starting the article, it is recommended that you have knowledge of React, Remix, Tailwind and a prior knowledge of nested routes and ORM's.

## Creating the Project

To initialize a project in Remix we execute the following command:

```sh
npx create-remix@latest remizzle
cd remizzle
```

We start the dev server with the following command:

```sh
npm run dev
```

## Set up Tailwind

We are going to use the `Just the basics` type, with a deployment target of `Remix App Server` and we are going to use `TypeScript` in the application.

Next, let's configure Tailwind in our project, starting with the installation:

```sh
npm install -D tailwindcss
npx tailwindcss init
```

Then we add the following in the `remix.config.js` file:

```js
module.exports = {
  future: {
    unstable_tailwind: true,
  },
};
```

We also make the following changes to `tailwind.config.js`:

```js
module.exports = {
  content: ["./app/**/*.{ts,tsx}"],
  // ...
};
```

Next, we create a file called `tailwind.css` inside the `app/` folder with the following:

```css
@tailwind base;
@tailwind components;
@tailwind utilities;
```

To make app development easier, let's take advantage of the pre-styled components of the daisyUI library, starting by installing the dependency:

```sh
npm install daisyui
```

Then we add the library to the list of plugins in the `tailwind.config.js` file in which we can also define which theme we want to use, as follows:

```js
module.exports = {
  // ...
  plugins: [require("daisyui")],
  daisyui: {
    themes: ["dracula"],
  },
};
```

Now, in the `root.tsx` file inside the `app/` folder, import the css file and add it to the `links` variable:

```tsx
// @/app/root.tsx
import type { LinksFunction } from "@remix-run/node";
// ...

import stylesheet from "~/tailwind.css";

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

// ...
```

## Set up Routes Gen

Remix uses file based routing, in which we define our application's routes through files that are created inside the `routes/` folder, with nested routes and URL parameters in the mix.

But in fact we don't have any type safety when we are navigating to a certain route or if we are missing some parameter, for that same reason we will take advantage of [Routes Gen](https://github.com/sandulat/routes-gen ) in this app.

First install the necessary dependencies:

```sh
npm install routes-gen @routes-gen/remix --dev 
```

In `package.json` we create the following script:

```js
{
  // ...
  "scripts": {
    // ...
    "routes:gen": "routes-gen -d @routes-gen/remix"
  },
  // ...
}
```

When we run the command, it will create the `routes.d.ts` file inside the `app/` folder with the data type declarations taking into account the application's routes. Remembering that the command is this:

```sh
npm run routes:gen
```


## Set up Drizzle ORM

To make this article accessible to more people, we are going to use the SQLite database and install the following dependencies:

```sh
npm install drizzle-orm better-sqlite3
npm install -D drizzle-kit @types/better-sqlite3
```

The next step will be to create the `config.server.ts` file in a folder called `db/` inside the `app/` folder, with the following:

```ts
// @/app/db/config.server.ts
import { drizzle } from "drizzle-orm/better-sqlite3";
import { migrate } from "drizzle-orm/better-sqlite3/migrator";
import Database from "better-sqlite3";

const sqlite = new Database("sqlite.db");

export const db = drizzle(sqlite);

migrate(db, { migrationsFolder: "./app/db/migrations" });
```

As you may have noticed in the code block above, we start by defining the database, then we use the SQLite driver and export its instance (`db`). 

Shortly afterwards we used the `migrator` function to carry out automatic migrations in our database taking into account what is generated inside the `app/db/migrations/` folder.

Then we define our database tables in the `schema.server.ts` file, also inside the `db/` folder, taking advantage of drizzle primitives and exporting the schema that was created:

```ts
// @/app/db/schema.server.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const dogs = sqliteTable("dogs", {
  id: integer("id").primaryKey(),
  name: text("name").notNull(),
  breed: text("breed").notNull(),
});
```

The last step in this section would be to create the following script in `package.json`:

```js
{
  // ...
  "scripts": {
    // ...
    "db:migrations": "drizzle-kit generate:sqlite --out ./app/db/migrations --schema ./app/db/schema.server.ts"
  },
  // ...
}
```

Running the above command will generate migrations inside the `app/db/migrations` folder that can be applied to the database. With this we can go to the next section.

## Modify and Build the Main Routes

In this section we will make some changes to existing pages, but we will also create new pages and new reusable components.

### Layout Page

First of all, let's create a file called `dogs.tsx` inside the `routes/` folder that will be the layout of our application, with the following:

```tsx
// @/app/routes/dogs.tsx
import { type V2_MetaFunction } from "@remix-run/node";
import { Outlet } from "@remix-run/react";

export const meta: V2_MetaFunction = () => {
  return [{ title: "Dog List Page" }];
};

export default function DogsLayout() {
  return (
    <div className="m-12">
      <Outlet />
    </div>
  );
}
```

In the example above we have a JSX component that returns only one `Outlet` and this will be responsible for rendering each of our app's nested routes, whose path starts with `/dogs`.

### App Index Page Redirect

Next, we go to the main route of our application to remove the JSX content, because we want a redirection from the `/` route to the `/dogs` route.

In the `_index.tsx` file, we do the following:

```tsx
// @/app/routes/_index.tsx
import { redirect } from "@remix-run/node";
import { route } from "routes-gen";

export const loader = () => redirect(route("/dogs"));
```

In order to have type safety with the routes, I recommend running the `routes:gen` command frequently to have the routes data types updated to take advantage of intellisense.

### Create-page

In this section we will create a reusable component called `Input` but first we need to install the following dependencies:

```sh
npm install remix-validated-form zod @remix-validated-form/with-zod
```

`remix-validated-form` is the form validation library that we are going to use, it is very simple, intuitive and has a collection of utility functions to help us deal with forms.

Then we create the following component inside the `app/components/` folder:

```tsx
// @/app/components/Input.tsx
import type { FC, InputHTMLAttributes } from "react";
import { useField } from "remix-validated-form";

interface InputProps extends InputHTMLAttributes<HTMLInputElement> {
  name: string;
  label: string;
}

export const Input: FC<InputProps> = ({ name, label, ...rest }) => {
  const { error, getInputProps } = useField(name);

  return (
    <span className="flex flex-col">
      <label htmlFor={name} className="mb-3">
        {label}
      </label>
      <input
        className="input input-bordered w-full max-w-xs"
        {...rest}
        {...getInputProps({ id: name })}
      />

      {error && <span className="label-text-alt mt-3">{error}</span>}
    </span>
  );
};
```

With this component we can move on to creating the page responsible for inserting a new doggo. The file to be created in the `routes/` folder is `dogs.create.tsx` with the following content:

```tsx
// @/app/routes/dogs.create.tsx
import type { DataFunctionArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { withZod } from "@remix-validated-form/with-zod";
import { ValidatedForm, validationError } from "remix-validated-form";
import { route } from "routes-gen";
import { z } from "zod";

import { Input } from "~/components/Input";
import { db } from "~/db/config.server";
import { dogs } from "~/db/schema.server";

const validator = withZod(
  z.object({
    name: z.string().min(1).max(34),
    breed: z.string().min(1).max(34),
  })
);

export const loader = () => {
  return json({
    defaultValues: {
      name: "",
      breed: "",
    },
  });
};

export const action = async ({ request }: DataFunctionArgs) => {
  const fieldValues = await validator.validate(await request.formData());
  if (fieldValues.error) return validationError(fieldValues.error);

  db.insert(dogs).values(fieldValues.data).run();

  return redirect(route("/dogs"));
};

export default function DogInsertion() {
  const { defaultValues } = useLoaderData<typeof loader>();

  return (
    <div>
      <div className="mb-8">
        <h2 className="mb-2 text-xl">Add a new doggo</h2>
        <p className="text-gray-600">Listen, every doggo is a good boy/girl.</p>
      </div>

      <ValidatedForm
        className="space-y-6"
        method="POST"
        validator={validator}
        defaultValues={defaultValues}
      >
        <Input name="name" label="Name" placeholder="Your doggo's name..." />
        <Input name="breed" label="Breed" placeholder="Your doggo's breed..." />

        <button className="btn btn-accent" type="submit">
          Submit
        </button>
      </ValidatedForm>
    </div>
  );
}
```

In the code block above, we started by defining the `validator` variable taking into account the validation schema of the form that needs to be filled in. 

Then, using the `loader` function, we return the default values of the form and with the `action` function, we obtain the data that were entered by the form and validate them. 

If the data is valid, we insert the new doggo and redirect the user, otherwise we deal with validation errors.

### Navigation

With the main components created, we can work on the `Navbar` which will help the user navigate between the list and insertion pages. So that it can later be used in `root.tsx`.

```tsx
// @/app/components/Navbar.tsx
import { Link } from "@remix-run/react";
import { route } from "routes-gen";

export const Navbar = () => (
  <nav className="navbar bg-base-300 rounded-xl mt-4">
    <div className="flex w-full items-center">
      <ul className="menu menu-horizontal px-1 space-x-4">
        <li>
          <Link to={route("/dogs")}>Dog List</Link>
        </li>
        <li>
          <Link to={route("/dogs/create")}>New Dog</Link>
        </li>
      </ul>
    </div>
  </nav>
);
```

As mentioned earlier, the next step is to import this newly created component into `root.tsx`:

```tsx
// @/app/root.tsx

// ...
import { Navbar } from "./components/Navbar";

// ...

export default function App() {
  return (
    <html lang="en">
      {/* ... */}
      <body>
        <div className="container mx-auto"> 
          <Navbar /> {/* 👈 added this line */}
          <Outlet />
        </div>

        {/* ... */}
      </body>
    </html>
  );
}
```

## Create the remaining routes

The effort in this section involves creating each of the remaining routes of our application, taking advantage of everything that has been done so far.

### Details Page

The purpose of this route is to display the data of a specific doggo taking into account the `dogId` parameter in the route. Let's take advantage of the `loader` function, in which we query the database and then return them so that they can be consumed in the UI using the `useLoaderData` hook.

```tsx
// @/app/routes/dogs.$dogId.details.tsx
import { json, type LoaderArgs } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { eq } from "drizzle-orm/expressions";
import type { RouteParams } from "routes-gen";

import { db } from "~/db/config.server";
import { dogs } from "~/db/schema.server";

export const loader = ({ params }: LoaderArgs) => {
  const { dogId } = params as RouteParams["/dogs/:dogId/details"];

  const result = db
    .select()
    .from(dogs)
    .where(eq(dogs.id, Number(dogId)))
    .get();

  return json(result);
};

export default function DogDetailsMain() {
  const data = useLoaderData<typeof loader>();

  return (
    <div className="w-3/5 lg:w-2/5">
      <h2 className="mb-6">Dog details:</h2>

      <div className="mockup-code">
        <pre data-prefix="~">
          <code>Name: {data.name}</code>,&nbsp;
          <code>Breed: {data.breed}</code>
        </pre>
      </div>
    </div>
  );
}
```

### Remove-page

The purpose of this page is to allow the user to remove a specific doggo. To do so, we will take advantage of the `action` function that will take the `dogId` parameter into account and we will delete the doggo from the database, after which it must be redirected to the main route of the nested routes.

```tsx
// @/app/routes/dogs.$dogId.remove.tsx
import type { ActionArgs } from "@remix-run/node";
import { redirect } from "@remix-run/node";
import { Form } from "@remix-run/react";
import { eq } from "drizzle-orm/expressions";
import { route, type RouteParams } from "routes-gen";

import { db } from "~/db/config.server";
import { dogs } from "~/db/schema.server";

export const action = ({ params }: ActionArgs) => {
  const { dogId } = params as RouteParams["/dogs/:dogId/remove"];

  db.delete(dogs)
    .where(eq(dogs.id, Number(dogId)))
    .run();

  return redirect(route("/dogs"));
};

export default function DogDetailsRemove() {
  return (
    <div>
      <label htmlFor="my-modal-6" className="btn btn-wide">
        Delete
      </label>

      <input type="checkbox" id="my-modal-6" className="modal-toggle" />
      <div className="modal modal-bottom sm:modal-middle">
        <div className="modal-box">
          <h3 className="font-bold text-lg">Delete Database Entry</h3>
          <p className="py-4">
            Are you sure you want to delete this database entry? This action is
            irreversible.
          </p>
          <div className="modal-action">
            <Form method="POST">
              <input type="hidden" name="_method" value="delete" />
              <button className="btn">Yes</button>
            </Form>
          </div>
        </div>
      </div>
    </div>
  );
}
```

### Update-page

The purpose of this route is to display data for a specific doggo taking into account the `dogId` parameter in the route. Using the `loader` we will obtain a doggo existing in the database and we will return it as JSON so that we have a previous filling of the form. As soon as changes are made, they are submitted and the record in the database is updated using an `action`.

```tsx
// @/app/routes/dogs.$dogId.update.tsx
import type { DataFunctionArgs, LoaderArgs } from "@remix-run/node";
import { json, redirect } from "@remix-run/node";
import { useLoaderData } from "@remix-run/react";
import { withZod } from "@remix-validated-form/with-zod";
import { ValidatedForm, validationError } from "remix-validated-form";
import type { RouteParams } from "routes-gen";
import { route } from "routes-gen";
import { eq } from "drizzle-orm/expressions";
import { z } from "zod";

import { Input } from "~/components/Input";
import { db } from "~/db/config.server";
import { dogs } from "~/db/schema.server";

const validator = withZod(
  z.object({
    name: z.string().min(1).max(34),
    breed: z.string().min(1).max(34),
  })
);

export const loader = ({ params }: LoaderArgs) => {
  const { dogId } = params as RouteParams["/dogs/:dogId/update"];

  const { id, ...rest } = db
    .select()
    .from(dogs)
    .where(eq(dogs.id, Number(dogId)))
    .get();

  return json({ defaultValues: rest });
};

export const action = async ({ request, params }: DataFunctionArgs) => {
  const fieldValues = await validator.validate(await request.formData());
  const { dogId } = params as RouteParams["/dogs/:dogId/update"];

  if (fieldValues.error) {
    return validationError(fieldValues.error);
  }

  db.update(dogs)
    .set(fieldValues.data)
    .where(eq(dogs.id, Number(dogId)))
    .run();

  return redirect(route("/dogs"));
};

export default function DogDetailsUpdate() {
  const { defaultValues } = useLoaderData<typeof loader>();

  return (
    <div>
      <h2 className="mb-6 text-xl">Update doggo entry</h2>

      <ValidatedForm
        className="space-y-6"
        method="POST"
        validator={validator}
        defaultValues={defaultValues}
      >
        <Input name="name" label="Name" placeholder="Your doggo's name..." />
        <Input name="breed" label="Breed" placeholder="Your doggo's breed..." />
        <button className="btn btn-accent" type="submit">
          Update
        </button>
      </ValidatedForm>
    </div>
  );
}
```

### Dog Details Layout

Now that we have each of the pages that make it possible to mutate the data in the database taking into account a specific doggo, we need to create navigation between the different pages that were created (details, remove and update).

Again, let's use the `route` primitive from `routes-gen` to autocomplete the editor to ensure that we define the necessary parameters for each of the routes.

Obviously we create each of the navigation items taking into account the created routes and use the `Outlet` primitive to render each of the nested routes.

```tsx
// @/app/routes/dogs.$dogId.tsx
import type { V2_MetaFunction } from "@remix-run/react";
import { Link, Outlet, useParams } from "@remix-run/react";
import type { RouteParams } from "routes-gen";
import { route } from "routes-gen";

export const meta: V2_MetaFunction = () => {
  return [{ title: "Dog Details Page" }];
};

export default function DogDetails() {
  const { dogId } = useParams<RouteParams["/dogs/:dogId"]>();

  return (
    <div className="flex flex-row space-x-12">
      <ul className="menu bg-base-100 w-56 rounded-box">
        <li>
          <Link
            to={route("/dogs/:dogId/details", {
              dogId: dogId ?? "",
            })}
          >
            Details
          </Link>
        </li>
        <li>
          <Link
            to={route("/dogs/:dogId/update", {
              dogId: dogId ?? "",
            })}
          >
            Update
          </Link>
        </li>
        <li className="text-red-600">
          <Link
            to={route("/dogs/:dogId/remove", {
              dogId: dogId ?? "",
            })}
          >
            Remove
          </Link>
        </li>
      </ul>

      <Outlet />
    </div>
  );
}
```

### Main Route

If you've followed the article so far, we've already created each of the routes to do the CRUD of the application, the only thing missing is to list the dogs that were added through the app in the database. To do so, let's create the following `dogs._index.tsx`:

```tsx
// @/app/routes/dogs.$dogId.tsx
import { json } from "@remix-run/node";
import { Link, useLoaderData } from "@remix-run/react";
import { route } from "routes-gen";

import { db } from "~/db/config.server";
import { dogs } from "~/db/schema.server";

export const loader = () => {
  const result = db.select().from(dogs).all();
  return json(result);
};

export default function DogList() {
  const data = useLoaderData<typeof loader>();

  return (
    <div className="overflow-x-auto">
      <table className="table table-zebra w-full">
        <thead>
          <tr>
            <th>ID</th>
            <th>Name</th>
            <th>Breed</th>
          </tr>
        </thead>

        <tbody>
          {data?.map((item) => (
            <tr key={item.id}>
              <th>
                <Link
                  to={route("/dogs/:dogId/details", {
                    dogId: item.id.toString(),
                  })}
                >
                  <kbd className="kbd">{item.id}</kbd>
                </Link>
              </th>
              <td>{item.name}</td>
              <td>{item.breed}</td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```

As you may have noticed in the code block above, we get all the data from the `dogs` table in the `loader` and return them so that we can render each of the table's rows on the page, as well as have a link to navigate to the pages of a doggo in detail.

## Conclusion

I hope you found this article helpful, whether you're using the information in an existing project or just giving it a try for fun.

Please let me know if you notice any mistakes in the article by leaving a comment. And, if you'd like to see the source code for this article, you can find it on the github repository linked below.

[Github Repo](https://github.com/FranciscoMendes10866/remizzle)