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 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 with a Neon serverless Postgres database. It focuses on how to set up the database and fetch data for a posts application using Prisma as the object-relational mapping (ORM) tool for efficient database management.
Prerequisite
To follow along with this guide, you should have:
- A fundamental understanding of HTML, CSS, and JavaScript
- Basic knowledge of React and TypeScript
- Node.js and npm (Node Package Manager) or 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 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:
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:
cd PostHive
npm install
3) Start the development server:
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:
/
|-- /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:
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:
const { posts } = useLoaderData();
The posts
data can then be looped through in your component for rendering:
<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:
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:
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:
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:
npm i prisma @prisma/client
2) Initialize a new Prisma project in your Remix project, creating a prisma
directory with a schema.prisma
file:
npx prisma init
After initializing, the schema.prisma
file will contain a default configuration:
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:
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.
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:
- Visit the Neon website and create an account if you haven’t already.
- Start a new project (e.g.,
postapp
). - Keep the version as 15, the latest release.
- Name your database (e.g.,
postappdb
). - 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:
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
:
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:
npm i tsx
Then execute the seed.ts
file:
tsx prisma/seed.t
Alternatively, you can add a Prisma script to your package.json
file:
"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:
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:
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:
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.
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.
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:
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:
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.
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:
<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:
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:
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. This application can incorporate more features, like the update post feature. Read through the Neon and Prisma documentation to learn more.
Have fun coding!