Building a full stack app with Remix & Drizzle ORM: Folder structure
Tags: react, remix, drizzle, typescript
Published: 2023-06-28 16:29:06

### Introduction
In this tutorial series, we'll explore building a full stack application using [Remix](https://remix.run/) and [Drizzle ORM](https://orm.drizzle.team/). In this tutorial, we will focus on creating the folder structure and React components that will be utilized in our future tutorials. Following the atomic design methodology, we will categorize our components into atoms (e.g., buttons and inputs), molecules (e.g., cards composed of atoms), and templates (e.g., users panel list, recent kudos list). By adhering to this structured approach, we can ensure modularity and reusability in our component architecture. Let's dive in and establish a solid foundation for building our application!

Credit for inspiring this tutorial series goes [Sabin Adams](https://twitter.com/sabinthedev), whose insightful [tutorial series](https://www.prisma.io/blog/fullstack-remix-prisma-mongodb-1-7D0BfTXBmB6r) served as a valuable source of inspiration for this project.

### Overview
Please note that this tutorial assumes a certain level of familiarity with React.js, Node.js, and working with ORMs. In this tutorial we will be - 
- Setting up atoms: Creating reusable components such as buttons, inputs, selects and avatar.
- Building molecules: Creating composite components like cards, composed of atoms, to represent specific UI elements.
- Designing templates: Developing template components such as users panel lists and recent kudos lists to structure larger sections of the application.
- Implementing service files: Creating service files to encapsulate database queries for users and kudos.

All the code for this tutorial can be found [here](https://github.com/yaldram/kudos-remix-drizzle/tree/feat/folder-structure-setup)

### Step 1: Setting up Atoms
![register-page](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/ixnl5nj8xc6egxp7dib6.png)
Now, let's focus on our signup page, which consists of inputs and buttons. These atomic components are highly versatile, as they can be utilized independently and across various sections of our application. Create a new folder `components` under the `app` folder, under `components` create a new folder called `atoms`. Within the `atoms` folder, create three files: -"Button.tsx", "InputField.tsx", "SelectField.tsx" and "Avatar.tsx" Additionally, create an "index.ts" file as an entry point in the `atoms` folder to export all these components. This structure allows for simplified imports across other files using `import { Button, InputField, ... } from "~/components/atoms"`. 
Under `components/atoms/Button.tsx` paste -
```tsx
type ButtonProps = React.ComponentPropsWithoutRef<"button">;

export function Button({ type, className, ...rest }: ButtonProps) {
  return (
    <button
      type={type}
      className={`rounded-xl mt-2 bg-yellow-300 disabled:opacity-50 disabled:cursor-not-allowed px-3 py-2 text-blue-600 font-semibold transition duration-300 ease-in-out hover:bg-yellow-400 hover:-translate-y-1 ${className}`}
      {...rest}
    />
  );
}
```
Under `components/atoms/InputField.tsx` paste - 
```tsx
type InputFieldProps = {
  label: string;
  error?: string;
  errorId?: string;
} & React.ComponentPropsWithoutRef<"input">;

export function InputField({
  name,
  id,
  label,
  type = "text",
  value,
  error = "",
  errorId,
  ...rest
}: InputFieldProps) {
  return (
    <>
      <label htmlFor={id} className="text-blue-600 font-semibold">
        {label}
      </label>
      <input
        type={type}
        id={id}
        name={name}
        className="w-full p-2 rounded-xl my-2"
        value={value}
        {...rest}
      />
      <div
        id={errorId}
        className="text-xs font-semibold text-center tracking-wide text-red-500 w-full"
      >
        {error}
      </div>
    </>
  );
}
```
Under `components/atoms/SelectField.tsx` paste -
```tsx
type SelectFieldProps = {
  label: string;
  containerClassName?: string;
  options: {
    name: string;
    value: string;
  }[];
  error?: string;
  errorId?: string;
} & React.ComponentPropsWithoutRef<"select">;

export function SelectField({
  id,
  label,
  options,
  containerClassName,
  className,
  name,
  error,
  errorId,
  ...delegated
}: SelectFieldProps) {
  return (
    <div>
      <label htmlFor={id} className="text-blue-600 font-semibold">
        {label}
      </label>
      <div className={`flex items-center ${containerClassName} my-2`}>
        <select
          className={`${className} appearance-none`}
          id={id}
          name={name}
          {...delegated}
        >
          {options.map((option, key) => (
            <option key={key} value={option.value}>
              {option.name}
            </option>
          ))}
        </select>
        <svg
          className="w-4 h-4 fill-current text-gray-400 -ml-7 mt-1 pointer-events-none"
          viewBox="0 0 140 140"
          xmlns="http://www.w3.org/2000/svg"
        >
          <g>
            <path d="m121.3,34.6c-1.6-1.6-4.2-1.6-5.8,0l-51,51.1-51.1-51.1c-1.6-1.6-4.2-1.6-5.8,0-1.6,1.6-1.6,4.2 0,5.8l53.9,53.9c0.8,0.8 1.8,1.2 2.9,1.2 1,0 2.1-0.4 2.9-1.2l53.9-53.9c1.7-1.6 1.7-4.2 0.1-5.8z" />
          </g>
        </svg>
      </div>
      <div
        id={errorId}
        className="text-xs font-semibold text-center tracking-wide text-red-500 w-full"
      >
        {error}
      </div>
    </div>
  );
}
```
Under `components/atoms/Avatar.tsx` paste - 
```tsx
import React from "react";

import type { UserProfile } from "~/drizzle/schemas/users.db.server";

type AvatarProps = {
  userProfile: UserProfile;
} & React.ComponentPropsWithoutRef<"div">;

export function Avatar(props: AvatarProps) {
  return (
    <div
      className={`${props.className} cursor-pointer bg-gray-400 rounded-full flex justify-center items-center`}
      onClick={props.onClick}
      style={{
        backgroundSize: "cover",
        ...(props.userProfile.profileUrl
          ? { backgroundImage: `url(${props.userProfile.profileUrl})` }
          : {}),
      }}
    >
      {!props.userProfile.profileUrl && (
        <h2>
          {props.userProfile.firstName.charAt(0).toUpperCase()}
          {props.userProfile.lastName.charAt(0).toUpperCase()}
        </h2>
      )}
    </div>
  );
}
```
Finally, export all the atoms from `components/atoms/index.ts` - 
```ts
export * from "./Avatar";
export * from "./Button";
export * from "./InputField";
export * from "./SelectField";
```

### Step 2: Building Molecules
Molecules are composite components composed of atoms. They represent specific UI elements that are more complex and reusable. You can create molecule components like cards, which can include multiple atoms such as buttons, avatars or texts.
![home-screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dypetm2al0e1vi8wrexf.png)
For our molecules, we will focus on three components related to the main page - 
- First, we have the `KudosCard` component, which represents a card displaying kudos. 
- When the user avatar is clicked, it triggers a modal component. Hence, the second molecule we will build is the `Modal` component.
- To implement the modal, we will utilize portals, ensuring it renders outside of its parent component's DOM hierarchy.
Under `components/molecules/KudoCard.tsx` paste -
```tsx
import type { Kudo } from "~/drizzle/schemas/kudos.db.server";
import type { UserProfile } from "~/drizzle/schemas/users.db.server";
import { backgroundColorMap, textColorMap, emojiMap } from "~/utils/constants";
import { Avatar } from "../atoms";

type KudoProps = {
  kudo: Pick<Kudo, "message" | "style">;
  userProfile: UserProfile;
};

export function KudoCard({ kudo, userProfile }: KudoProps) {
  return (
    <div
      className={`flex ${
        backgroundColorMap[kudo.style.backgroundColor]
      } p-4 rounded-xl w-full gap-x-2 relative`}
    >
      <div>
        <Avatar userProfile={userProfile} className="h-16 w-16" />
      </div>
      <div className="flex flex-col">
        <p
          className={`${
            textColorMap[kudo.style.textColor]
          } font-bold text-lg whitespace-pre-wrap break-all`}
        >
          {userProfile.firstName} {userProfile.lastName}
        </p>
        <p
          className={`${
            textColorMap[kudo.style.textColor]
          } whitespace-pre-wrap break-all`}
        >
          {kudo.message}
        </p>
      </div>
      <div className="absolute bottom-4 right-4 bg-white rounded-full h-10 w-10 flex items-center justify-center text-2xl">
        {emojiMap[kudo.style.emoji]}
      </div>
    </div>
  );
}
```
Under `atoms/molecules/Portal.tsx` paste -
```tsx
import { createPortal } from "react-dom";
import { useState, useEffect } from "react";

type PortalProps = {
  children: React.ReactNode;
  wrapperId: string;
};

const createWrapper = (wrapperId: string) => {
  const wrapper = document.createElement("div");
  wrapper.setAttribute("id", wrapperId);
  document.body.appendChild(wrapper);
  return wrapper;
};

export function Portal({ children, wrapperId }: PortalProps) {
  const [wrapper, setWrapper] = useState<HTMLElement | null>(null);

  useEffect(() => {
    let element = document.getElementById(wrapperId);
    let created = false;

    if (!element) {
      created = true;
      element = createWrapper(wrapperId);
    }

    setWrapper(element);

    return () => {
      if (created && element?.parentNode) {
        element.parentNode.removeChild(element);
      }
    };
  }, [wrapperId]);

  if (wrapper === null) return null;

  return createPortal(children, wrapper);
}
```
Under `atoms/molecules/Modal.tsx` paste - 
```tsx
import { Portal } from "./Portal";

type ModalProps = {
  children: React.ReactNode;
  isOpen: boolean;
  ariaLabel?: string;
  className?: string;
  onOutsideClick: () => void;
};

export function Modal({
  children,
  isOpen,
  ariaLabel,
  onOutsideClick,
  className,
}: ModalProps) {
  if (!isOpen) return null;

  return (
    <Portal wrapperId="modal">
      <div
        className="fixed inset-0 overflow-y-auto bg-gray-600 bg-opacity-80"
        aria-labelledby={ariaLabel ?? "modal-title"}
        role="dialog"
        aria-modal="true"
        onClick={() => onOutsideClick()}
      ></div>
      <div className="fixed inset-0 pointer-events-none flex justify-center items-center max-h-screen overflow-scroll">
        <div
          className={`${className} p-4 bg-gray-200 pointer-events-auto max-h-screen md:rounded-xl`}
        >
          {children}
        </div>
      </div>
    </Portal>
  );
}
```
Finally, export all molecules from `components/molecules/index.ts` 
```ts
export * from "./KudoCard";
export * from "./Modal";
export * from "./Portal";
```
### Step 3: Setup Templates
Templates are higher-level components that structure larger sections of the application. They provide a layout and organization for the components within them. 
![home-screen](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/dypetm2al0e1vi8wrexf.png)
Within our app, we have three templates arranged from left to right: - 
- Firstly, the `UsersPanel` template showcases a list of users with their avatars. 
- Next, we have the `SearchPanel` template at the top displaying the logged in user profile to the right.
- Lastly, on the right-hand side, we have the `RecentKudosPanel` template, featuring a list of recent kudos along with user avatars and kudo emojis. 
Under `components/templates/UsersPanel.tsx` paste -
```tsx
import type { User } from "~/drizzle/schemas/users.db.server";
import { getUserProfile } from "~/utils/helpers";
import { Button, Avatar } from "../atoms";

type UsersPanelProps = {
  users: User[];
};

export function UsersPanel(props: UsersPanelProps) {
  return (
    <>
      <div className="text-center bg-gray-300 h-20 flex items-center justify-center">
        <h2 className="text-xl text-blue-600 font-semibold">My Team</h2>
      </div>
      <div className="flex-1 overflow-y-scroll py-4 flex flex-col gap-y-10">
        {props.users.map((user) => (
          <Avatar
            key={user.id}
            userProfile={getUserProfile(user)}
            className="h-24 w-24 mx-auto flex-shrink-0"
          />
        ))}
      </div>
      <div className="text-center p-6 bg-gray-300">
        <form action="/logout" method="post">
          <Button type="submit">Logout</Button>
        </form>
      </div>
    </>
  );
}
```
Don't worry about the logout button we will implement the functionality later. Under `utils/helpers.ts` create `getUserProfile` function -
```ts
import type { User } from "~/drizzle/schemas/users.server";

export function getUserProfile(user: User) {
  return {
    firstName: user.firstName,
    lastName: user.lastName,
    profileUrl: user.profileUrl,
  };
}
```
Now under `components/templates/RecentKudosPanel.tsx` paste -
```tsx
import { getUserProfile } from "~/utils/helpers";
import { emojiMap } from "~/utils/constants";
import type { User } from "~/drizzle/schemas/users.db.server";
import { Avatar } from "../atoms";

type RecentBarProps = {
  records: any;
};

export function RecentKudosPanel({ records }: RecentBarProps) {
  return (
    <div className="w-1/5 border-l-4 border-l-yellow-300 flex flex-col items-center">
      <h2 className="text-xl text-yellow-300 font-semibold my-6">
        Recent Kudos
      </h2>
      <div className="h-full flex flex-col gap-y-10 mt-10">
        {records.map(({ kudos, users }: any) => (
          <div className="h-24 w-24 relative" key={kudos.id}>
            <Avatar
              userProfile={getUserProfile(users as User)}
              className="w-20 h-20"
            />
            <div className="h-8 w-8 text-3xl bottom-2 right-4 rounded-full absolute flex justify-center items-center">
              {emojiMap[kudos.style.emoji]}
            </div>
          </div>
        ))}
      </div>
    </div>
  );
}
```
Currently, the props for our components have the type "any." We will update and assign proper types to these props later when we add the respective functionality.
Under `components/templates/SearchPanel.tsx` paste -
```tsx
import type { User } from "~/drizzle/schemas/users.db.server";
import { getUserProfile } from "~/utils/helpers";
import { Avatar } from "../atoms";

type SearchBarProps = {
  user: User;
};

export function SearchBar(props: SearchBarProps) {
  return (
    <div className="w-full px-6 items-center gap-x-4 border-b-4 border-b-yellow-300 h-20 flex justify-end p-2">
      <Avatar
        className="h-14 w-14 transition duration-300 ease-in-out hover:scale-110 hover:border-2 hover:border-yellow-300"
        userProfile={getUserProfile(props.user)}
      />
    </div>
  );
}
```
Finally under `components/templates/index.ts` paste -
```ts
export * from "./SearchPanel";
export * from "./UsersPanel";
export * from "./RecentKudosPanel";
```
Great job! With the completion of all our components, we are now ready to utilize them in the upcoming tutorials.

### Step 4: Create service files
In our application, we have two tables, `kudos` and `users`, which require database queries. To maintain a clean and organized structure, we will create separate service files for each table under the `services` folder. Additionally, we will create a `sessions.server.ts` file to handle authentication sessions using Remix. Create a `services` folder under `app` folder and create 3 files namely - `kudos.server.ts`, `users.server.ts` & `sessions.server.ts`. 

It's important to note that we should add the `.server` extension to our service files. By doing so, Remix will exclude these files from the client bundle, ensuring they are only included on the server. We will delve deeper into this topic and its significance in the later tutorials.

### Conclusion

In this tutorial, we established a well-structured folder hierarchy, developed essential components following the atomic design methodology, and added service files for future db queries. In the next tutorial, we will focus on user registration and login functionality, building upon the foundation we have laid. All the code for this tutorial can be found [here](https://github.com/yaldram/kudos-remix-drizzle/tree/feat/folder-structure-setup). Until next time PEACE!