Building a full stack app with Remix, Prisma, and Neon
Published: 2024-03-12 09:43:00

In modern web development, scalable applications are paramount due to the ever-increasing demand for flexible, responsive systems that can handle growing user data and evolving functionalities. One way to achieve this involves leveraging technologies like Remix, Prisma, and Neon. 

Remix enhances web application development with a user-centric and developer-friendly approach. Prisma offers streamlined data management for scalable growth, whereas Neon's serverless PostgreSQL ensures effortless scaling and reduced database complexity. By combining these tools, developers can build a robust ecosystem that empowers the crafting of such applications.

React and [JavaScript frameworks](https://kinsta.com/blog/javascript-libraries/) have always reigned supreme for developing fast-loading apps. However, they often require separate server code for database or API interaction. Server-side rendering frameworks like Remix streamline this process by allowing you to combine client and server-side functionalities within a project.

This guide walks you through integrating [Remix](https://remix.run/?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) with a [Neon](https://neon.tech/?ref=hm) serverless Postgres database. It focuses on how to set up the database and fetch data for a posts application using [Prisma](https://www.prisma.io/?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) as the object-relational mapping (ORM) tool for efficient database management.


![A demo of a fullstack application built with Remix, Prisma, and Neon](https://paper-attachments.dropboxusercontent.com/s_FD76DEB125CE6FC9894E7B3D466EB7FC53695F6A1671DD83D181CBDF6130F055_1701119042239_2023-11-27_22-02-53+1.gif)

## Prerequisite

To follow along with this guide, you should have:

- A fundamental understanding of [HTML](https://kinsta.com/knowledgebase/what-is-html/), [CSS](https://kinsta.com/blog/css-best-practices/), and [JavaScript](https://kinsta.com/knowledgebase/what-is-javascript/)
- Basic knowledge of [React](https://react.dev/) and [TypeScript](https://www.typescriptlang.org/)
- Node.js and npm ([Node Package Manager](https://kinsta.com/knowledgebase/what-is-npm/)) or [yarn](https://kinsta.com/blog/web-development-tools/#39-yarn) installed on your computer

## Setting up the environment

To get started with this guide, having a Remix project is essential. If you already have a Remix project, skip to the “Setting up Prisma in Remix” section. If not, clone the [starter project](https://github.com/olawanlejoel/PostHive/tree/starter-files?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) prepared for this guide. *The complete project is at the end of this tutorial*.

The starter project contains basic Remix code, including pages and layout files with styles. To clone the project to your local machine, follow these steps:


1) Navigate to your preferred directory for storing your code and execute the following command in your terminal:

```bash
git clone -b starter-files --single-branch https://github.com/olawanlejoel/PostHive.git
```

2) Move into the project folder and install all the required dependencies:

```bash
cd PostHive
npm install
```

3) Start the development server:

```bash
npm run dev
```

The local development server starts on http://localhost:3000/ where you can access your Remix app.

## Understanding the Remix file structure

You will notice some standard files for every Remix project, but focus on the `app` folder, which is the heart of every Remix application.

In the `app` folder, you will get the following structure:

```bash
/
|-- /app
    |-- /routes
        |-- _index.tsx
        |-- posts._index.tsx
        |-- posts.$postid.tsx
        |-- posts.new.tsx
        |-- posts.tsx
    |-- /styles
        |-- global.css
    |-- entry.client.tsx
    |-- entry.server.tsx
    |-- root.tsx
```

- `entry.client.tsx`: Serves as the entry point for client-side code, mainly for browser rendering. We'll stick to the basic code here and won't delve deeper.
- `entry.server.tsx`: The entry point for server-side rendering, responsible for initial server rendering. We'll focus on basic code here as well.
- `root.tsx`: This file represents the root component of your application, shaping the primary UI structure. Here, we've included the navbar code, intending to be global across all pages, and imported the CSS file.
- `/styles`: This directory holds your application's stylesheets or CSS files.
- `/routes`: Manages various routes or pages within your app. Any file created in this folder serves as a route.
    - `_index.tsx`: Represents the index or home page, displaying some text and a button.
    - `posts._index.tsx`: Displays an array of all posts. It should retrieve these posts from the Neon database and showcase them within the Remix application.
    - `posts.$postid.tsx`: Manages individual posts, where `$postid` acts as a placeholder for the post ID. Configuration for this occurs when integrating the server-side actions.
    - `posts.new.tsx`: Displays a form for creating new posts to be stored in the database.
    - `posts.tsx`: This serves as the layout for the posts pages.

## Loaders and Actions in Remix

Understanding Loaders and Actions in Remix is pivotal for effective interaction with databases like Prisma and Neon. These concepts are crucial in managing data retrieval and executing necessary operations within the database.

### Loaders: Fetching data with `useLoaderData`
Within the `posts._index.tsx` file, a loader function is created to declare the `posts` array and access its value using the `useLoaderData` function from Remix.

`useLoaderData` is a function in Remix that facilitates data fetching from various sources, including databases such as Neon via Prisma. By leveraging this function, developers can efficiently load necessary data for rendering components or executing actions in their applications.

Here's a snippet demonstrating its usage:

```js
import { useLoaderData } from '@remix-run/react';

export const loader = async () => {
    const data = {
        posts: [
            // all posts
        ],
    };
    return data;
};
```

This server-side code enables data access on the client side using the `useLoaderData` function:

```js
const { posts } = useLoaderData();
```

The `posts` data can then be looped through in your component for rendering:

```html
<ul className="posts-list">
    {posts.map((post) => (
        <li key={post.id}>
            <Link to={post.id}>
                <h3>{post.title}</h3>
                {new Date(post.createdAt).toLocaleString()}
            </Link>
        </li>
    ))}
</ul>
```

> **Note:** This is a TypeScript file, so you’ll notice type checkings in the `starter-files` code.

### Action: Handling form data submission
In the `posts.new.tsx` file, a form is present. To handle data submission from this form, Remix employs `action`. Similar to the `loader`, `action` facilitates operations or modifications within the database.

To use `action`, you’d create a function and export it:

```js
export const action = () => {
    // server actions
    return {};
};
```

You can get the data from a form by attaching a POST method and a `type` of `submit` to the button. You will also give each field a `name`, which can then be accessed in the `action` function using the `request.``*formData*``()` method:

```js
export const action = async ({ request }) => {
    const form = await request.formData();
    const title = form.get('title');
    const body = form.get('body');

    const fields = { title, body};

    console.log(fields);
    return {};
};
```

When you fill out the form and submit it, you will notice the values logged to the console of your terminal (not on the client side because it’s server-side code). You can import the `redirect` function to return redirect so once the form is submitted, it redirects you to another page:

```js
import { redirect } from '@remix-run/node';

export const action = async ({ request }) => {
    const form = await request.formData();
    const title = form.get('title');
    const body = form.get('body');

    const fields = { title, body};

    console.log(fields);

    return redirect('/posts');
};
```

In the above, we have not performed any significant action, but when dealing with databases, `action` will involve creating new database entries, updating existing records, deleting data, or executing complex queries to modify the database.

> **Note:** Type-checking is not implemented in the snippet above, but remember to implement it since this is a TypeScript file. 

Now that you understand how loaders and actions work, it's time to explore using Prisma and Neon with Remix.

## Setting up Prisma in Remix

Prisma is a modern database toolkit that simplifies database access for developers, offering an intuitive interface to work with databases while providing a type-safe and powerful query API. 

It supports databases like PostgreSQL, MySQL, SQLite, and SQL Server, allowing seamless interaction through its ORM (Object-Relational Mapping) capabilities. To set up Prisma, follow these steps:


1) Open your terminal and install Prisma alongside Prisma Client:

```bash
npm i prisma @prisma/client
```

2) Initialize a new Prisma project in your Remix project, creating a `prisma` directory with a `schema.prisma` file:

```bash
npx prisma init
```

After initializing, the `schema.prisma` file will contain a default configuration:

```prisma
generator client {
    provider = "prisma-client-js"
}

datasource db {
    provider = "postgresql"
    url      = env("DATABASE_URL")
}
```

3) Next, define a basic `Post` model in Prisma that mirrors your database's structure. Add the following code to your `schema.prisma` file:

```prisma
model Post {
    id        String    @id @default(uuid())
    title     String
    body      String
    createdAt DateTime  @default(now())
    updatedAt DateTime  @updatedAt
}
```

The `Post` model defines essential attributes: a unique ID generated by `uuid()`, fields for `title` and `body` containing post content, and timestamps `createdAt` and `updatedAt` for creation and modification times.

To enable this Prisma configuration, create a `.env` file in your Remix project's root directory if one doesn't exist. This file should contain your Postgres database URL. You have not created the Postgres database; you'd do that with Neon.

```bash
DATABASE_URL='postgresql://USER:PASSWORD@HOST:PORT/DATABASE'
```

## Setting up a Postgres database with Neon 

Neon is a fully managed, serverless PostgreSQL database. It separates storage and computing, offering unlimited autoscaling, branching, and storage. 

Its serverless architecture allows instantaneous database creation, connection, and dynamic scaling computing based on usage to optimize costs and integrates with cloud object stores like S3 for cost-effective storage. 

To set up a Postgres database, follow these steps:

1. Visit the [Neon websit](https://neon.tech/?ref=hm)e and create an account if you haven’t already.
2. Start a new project (e.g., `postapp`).
3. Keep the version as 15, the latest release.
4. Name your database (e.g., `postappdb`).
5. Choose your preferred Region and click **Create project**.

A modal will pop up, displaying the Postgres database URL. Copy this URL and save it in your `.env` file. Ensure you only copy the URL and remove `psql`.

With this, you have successfully created a Postgres database.

## Seeding your Neon database with Prisma

Your Prisma project is currently configured, and your Postgres database is set up with Neon. Next, seed the database with primary data to test everything before diving into Remix interactions.

Start by creating a `posts.ts` file to contain an array of posts you want to seed into the database. Export the array like so:

```ts
export const posts = [
    {
        title: 'JavaScript Performance Tips',
        body: `We will look at 10 simple tips and tricks to increase the speed of your code when writing JS`,
    },
    {
        title: 'Tailwind vs. Bootstrap',
        body: `Both Tailwind and Bootstrap are very popular CSS frameworks. In this article, we will compare them`,
    },
    {
        title: 'Writing Great Unit Tests',
        body: `We will look at 10 simple tips and tricks on writing unit tests in JavaScript`,
    },
    {
        title: 'What Is New In PHP 8?',
        body: `In this article we will look at some of the new features offered in version 8 of PHP`,
    },
];
```

Next, create a `seed.ts` file in the `prisma` folder with this configuration using `PrismaClient`:

```ts
import { posts } from './posts';
import { PrismaClient } from '@prisma/client';
const prisma = new PrismaClient();
async function main() {
    for (let post of posts) {
        await prisma.post.create({
            data: post,
        });
    }
}
main()
    .catch((e) => {
        console.log(e);
        process.exit(1);
    })
    .finally(() => {
        prisma.$disconnect();
    });
```

This `seed.ts` file imports the `posts` array, then uses `PrismaClient` to iterate through each post in the array and adds it to the database via `prisma.post.create()`.

Next, you need to run the `seed.ts` file. To run this TypeScript file, install the `tsx` dependency:

```bash
npm i tsx
```

Then execute the `seed.ts` file:

```bash
tsx prisma/seed.t
```

Alternatively, you can add a Prisma script to your `package.json` file:

```json
"prisma": {
    "seed": "tsx prisma/seed.ts"
},
```

When you run `npx prisma db seed`, it will populate your Neon database. To check, reload your Neon database, navigate to the Tables in your dashboard, and click the Post table to view the data:


![Seeded data showing in Neon database](https://paper-attachments.dropboxusercontent.com/s_FD76DEB125CE6FC9894E7B3D466EB7FC53695F6A1671DD83D181CBDF6130F055_1701109999171_neon-seed-sb.jpg)


This approach lets you pre-fill your database with the specified data, ensuring your setup works seamlessly before delving into Remix interactions.

## Interacting with Prisma and Neon from Remix

Now that you've set up a Neon Postgres database, Prisma, and some initial data seeding to verify its functionality, the next step involves fetching these posts from Neon into your Remix app. You'll also implement the functionality to add and delete posts.

Start by creating a server file responsible for establishing a database connection using Prisma:

```js
import { PrismaClient } from '@prisma/client';

let db: PrismaClient;

declare global {
    var __db: PrismaClient | undefined;
}

if (process.env.NODE_ENV == 'production') {
    db = new PrismaClient();
    db.$connect();
} else {
    if (!global.__db) {
        global.__db = new PrismaClient();
        global.__db.$connect();
    }
    db = global.__db;
}

export { db };
```

This code ensures a consistent database connection throughout the application's lifecycle. In production, it directly establishes a new connection; in other environments, it maintains a single global connection for efficiency.

### Loading posts with Remix loader
In the `posts._index.tsx` file, to access the database, import the database schema, and update the posts array:

```js
import { useLoaderData, Link } from '@remix-run/react';

import { db } from '~/utils/db.server';
// Define a type for a single post
type Post = {
    id: string;
    title: string;
    body: string;
    createdAt: Date;
};

// Define a type for the data returned by the loader
type LoaderData = {
    posts: Post[];
};

export const loader = async () => {
    const data = {
        posts: await db.post.findMany(),
    };

    return data;
};
```

This code sets up a loader function to fetch an array of posts from the database using `db.post.findMany()`, defining types for the fetched data. The fetched `posts` data is then made available to components through the loader.

```js
const Posts = () => {
    const { posts } = useLoaderData<LoaderData>();

    return (
        <>
            <div className="page-header">
                <h1>Posts</h1>
                <Link to="/posts/new" className="btn">
                    New Post
                </Link>
            </div>
            <ul className="posts-list">
                {posts.map((post) => (
                    <li key={post.id}>
                        <Link to={post.id}>
                            <h3>{post.title}</h3>
                            {new Date(post.createdAt).toLocaleString()}
                        </Link>
                    </li>
                ))}
            </ul>
        </>
    );
};

export default Posts;
```

This will display all the posts stored in the Postgres database on your Remix application.


![All posts fetched from Neon via Prisma and displayed in Remix application](https://paper-attachments.dropboxusercontent.com/s_FD76DEB125CE6FC9894E7B3D466EB7FC53695F6A1671DD83D181CBDF6130F055_1701113325935_image.png)


### Load a single post from the database
In the `posts.$postid.tsx` file, which serves as a dynamic route to display each post based on its ID, you'll implement the retrieval of dynamic data from the database:

In the `loader` function, you’ll use the `findUnique()` method by attaching the `where` parameter indicating the `ID` for the post to be fetched:

```js
import type { LoaderFunction } from '@remix-run/node';
import { useLoaderData, Link, Form } from '@remix-run/react';
import { db } from '~/utils/db.server';

export const loader: LoaderFunction = async ({ params }) => {
    const post = await db.post.findUnique({
        where: { id: params.postid },
    });

    if (!post) throw new Error('Post not found');

    const data = { post };

    return data;
};
```

You can then use the `useLoaderData` function to populate the client side of the application:

```js
interface Post {
    title: string;
    body: string;
    // Define other properties if available in the 'post' object
}

const Postid = () => {
    const { post }: { post: Post } = useLoaderData();

    return (
        <>
            <div className="page-header">
                <h2>{post.title}</h2>
                <Link to="/posts" className="btn btn-reverse">
                    Back
                </Link>
            </div>
            <div className="page-content">{post.body}</div>
            <div className="page-footer">
                <button className="btn btn-delete">Delete</button>
            </div>
        </>
    );
};

export default Postid;
```

When you click any post from the Posts index page, it will redirect you to this dynamic page with the unique data.


![Single posts fetched via it’s unique ID](https://paper-attachments.dropboxusercontent.com/s_FD76DEB125CE6FC9894E7B3D466EB7FC53695F6A1671DD83D181CBDF6130F055_1701113997073_image.png)


### Deleting posts with Remix action
Implementing the **Delete** button on the dynamic page involves steps similar to those used to fetch a dynamic post. However, this time, `action` is utilized. You’ll wrap the button in a form such that a post request can be submitted to trigger the button action:

```html
<div className="page-footer">
    <Form method="POST">
        <input type="hidden" name="_method" value="delete" />
        <button className="btn btn-delete">Delete</button>
    </Form>
</div>
```

The form sends a `POST` request with a hidden input field (`_method`) set to `delete` to indicate the intention to delete the post. This is what the server-side code will look like:

```js
import type { ActionFunction } from '@remix-run/node';
import { redirect } from '@remix-run/node';

import { db } from '~/utils/db.server';

export const action: ActionFunction = async ({ request, params }) => {
    const form = await request.formData();

    if (form.get('_method') === 'delete') {
        const post = await db.post.findUnique({
            where: { id: params.postid },
        });
        if (!post) throw new Error('Post not found');
        await db.post.delete({
            where: { id: params.postid },
        });
        return redirect('/posts');
    }
};
```

On the server side, when a `POST` request is received, it checks that it contains a `_method` field set to "delete". If found, it retrieves the post based on the specified `postid`, ensures its existence, and then proceeds to delete it from the database using `db.post.delete`. Finally, it redirects the user to the `/posts` page after successful deletion.

This approach follows a RESTful convention, utilizing a POST request with a hidden field to simulate a DELETE request, enabling the server to process the delete action accordingly.

When you click the delete button, the post will be deleted from the database and on the client, redirecting you to the posts index page.

### Adding a new post with Remix action
Finally, one prominent feature of every full-stack application is being able to add new information from the client side to your database. This can be done with the `create()` method, specifying the form fields:

```js
import type { ActionFunction } from '@remix-run/node';
import { redirect } from '@remix-run/node';
import { Link, Form } from '@remix-run/react';
import { db } from '~/utils/db.server';

export const action: ActionFunction = async ({ request }) => {
    const form = await request.formData();
    const title = form.get('title') ?? '';
    const body = form.get('body') ?? '';
    const fields = {
        title: String(title),
        body: String(body),
    };

    const post = await db.post.create({ data: fields });

    return redirect(`/posts/${post.id}`);
};
```

The code above utilizes Remix's `action` function to handle form submissions, extracting `title` and `body` data from a client request and then creating a new post in the database using this information. Upon successful creation, it redirects the user to the newly generated post's URL.

## Conclusion

This guide explores building a full-stack application with Remix utilizing Prisma and Neon. This full-stack approach, incorporating Remix's capabilities, Prisma's robust ORM, and Neon's database agility, provides a solid foundation for building modern, scalable web applications.

You can access the [complete code for this application on GitHub](https://github.com/olawanlejoel/PostHive?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity). This application can incorporate more features, like the update post feature. Read through the [Neon](https://neon.tech/docs/introduction?ref=hm) and [Prisma documentation](https://www.prisma.io/docs?utm_source=hackmamba&utm_medium=blog&utm_id=HMBcommunity) to learn more.

Have fun coding!