Saving new entries
Set up Prisma and SQLite to persist our dynamic data.
19 lessons · 5:00:25
- 1Scaffolding a new app12:31
- 2Creating an entry form22:49
- 3Saving new entries20:44
- Adding pending UI15:12
- Loading entries8:22
- Displaying entries by week24:09
- Creating the edit page13:43
- Editing entries9:47
- Making the entry form reusable10:20
- Deleting entries10:25
- Adding an authenticated state23:43
- Adding a logout button6:48
- Customizing the public UI5:01
- Securing our app12:58
- Customizing the error pages13:19
- Adding error messages to the login form13:37
- Mobile redesign41:57
- Desktop redesign18:46
- Deploying our app16:14
We're ready to persist the data from our form to a database using Prisma and SQLite.
Installing Prisma and SQLite
Let's install Prisma into our project:
npm install prisma --save-dev
and initialize it with the SQLite provider:
npx prisma init --datasource-provider sqlite
This scaffolds a schema file for us at prisma/schema.prisma
:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
and if we check our .env
file, we'll see that the init
script went ahead and set the DATABASE_URL
environment variable for us.
If you haven't already, be sure to install the official Prisma Extension for VSCode, which gives us syntax highlighting and autocomplete in our schema.prisma
file.
Prisma and SQLite are set up and ready for us to use!
Modeling our data
We're ready to model our data. Let's create an Entry
model to save the data from our <EntryForm>
:
// prisma/schema.prisma
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "sqlite"
url = env("DATABASE_URL")
}
model Entry {
id Int @id @default(autoincrement())
date DateTime
type String
text String
}
Entry
has four fields:
id
: an integer that represents the primary key of the model (marked by the@id
attribute). It also assigns a default auto-incrementing integer for newly created records.date
: the date for the entry. Even though our form only supports the date but not the time, we're using theDateTime
type because it's supported out of the box with Prisma and SQLite.type
: stores which category an Entry belongs to – "work", "learning", or "interesting-thing".text
: the actual content of the Entry.
This is a perfect start for saving all the data our user can enter in our <EntryForm>
.
Now that we've defined our Entry model in Prisma, we need to tell SQLite about it. Prisma has a db push command that's perfect for prototyping new apps, so let's run that command now:
npx prisma db push
We'll see that Prisma has created a dev.db
file in our project. This is our new SQLite database that Prisma created for us using everything we defined in our schema file – much easier than us writing out SQL CREATE TABLE
statements by hand!
To see our new database in action, let's run
npx prisma studio
This command opens a new tab at localhost:5555
with a nice admin UI that comes bundled with Prisma. We can see all of our models, create new records in our SQLite database, and even delete them.
Now that Prisma and SQLite are wired up, we're ready to programmatically save data from our Remix app!
Using @prisma/client
to create entries
To use Prisma from our Remix app, we'll need one more package: Prisma Client.
Let's install it now:
npm install @prisma/client
Now let's come to our Index route and instantiate it:
// app/routes/index.tsx
import { PrismaClient } from "@prisma/client";
let db = new PrismaClient();
You'll notice that db
is fully typed with our Entry model, so we get autocomplete for creating, reading, updating, and deleting Entry records, as well as typesafe access to all the fields we defined. This is one of the biggest benefits of using Prisma.
Let's come to our action
and try to create a new Entry record with some static data:
import { PrismaClient } from "@prisma/client";
let db = new PrismaClient();
export async function action({ request }: ActionArgs) {
let formData = await request.formData();
let data = Object.fromEntries(formData);
db.entry.create({
data: {
date: "2023-03-07",
type: "work",
text: "some text",
},
});
}
We'll start our Remix app with npm run dev
, visit the Index route, and click Save.
If we look in the browser console, we'll see an error:
Uncaught Error: PrismaClient is unable to be run in the browser.
We're seeing this error because Prisma is designed to only ever run in the server (since it needs a direct connection to the database), but Remix is trying to run it in the browser as well.
This is happening because we're instantiating Prisma in module scope, outside of our action:
import { PrismaClient } from "@prisma/client";
let db = new PrismaClient();
export async function action({ request }: ActionArgs) {
//
}
Because our Index route contains both server-side and client-side code, running code in module scope is ambiguous and Remix defaults to executing it in both environments.
We can fix this by moving our new PrismaClient()
code to within the action
, since actions only ever run on the server:
import { PrismaClient } from "@prisma/client";
- let db = new PrismaClient();
export async function action({ request }: ActionArgs) {
+ let db = new PrismaClient();
let formData = await request.formData();
let data = Object.fromEntries(formData);
db.entry.create({
data: {
date: "2023-03-07",
type: "work",
text: "some text",
},
});
}
If we save this and try submitting the form again, we don't see the error anymore. But if we check Prisma studio, we also don't see any new records in our database.
If we look at what db.entry.create
responds with, we'll see it returns a Promise. So we need to await
it:
import { PrismaClient } from "@prisma/client";
export async function action({ request }: ActionArgs) {
let db = new PrismaClient();
let formData = await request.formData();
let data = Object.fromEntries(formData);
await db.entry.create({
data: {
date: "2023-03-07",
type: "work",
text: "some text",
},
});
}
If we give this another shot, we'll see a new error:
Argument date: Got invalid value of "2023-03-07". Provided String, expected DateTime
We're seeing this because Prisma expects a JavaScript Date object to map to its own DateTime
type. So let's create a new Date
for this argument:
import { PrismaClient } from "@prisma/client";
export async function action({ request }: ActionArgs) {
let db = new PrismaClient();
let formData = await request.formData();
let data = Object.fromEntries(formData);
await db.entry.create({
data: {
date: new Date("2023-03-07"),
type: "work",
text: "some text",
},
});
}
Now if we submit the form, we don't see any errors, and if we come back to Prisma Studio, we see our new record!
Let's replace the static data with the data from our form:
import { PrismaClient } from "@prisma/client";
export async function action({ request }: ActionArgs) {
let db = new PrismaClient();
let formData = await request.formData();
let data = Object.fromEntries(formData);
await db.entry.create({
data: {
date: new Date(data.date),
type: data.type,
text: data.text,
},
});
}
Let's refresh the app, choose a date, choose Interesting thing, and enter some text: "Chat GPT just got a lot faster!". Once we click save and check out Prisma Studio, we'll see a new entry in our backend with the data from the form.
Pretty neat! We're now getting user-submitted data from our frontend form into our database via our action.
Addressing TypeScript errors
Before wrapping up, let's address some errors that Typescript is pointing out.
If we hover over the data
argument to db.entry.create()
, we'll see that each value we pass in from our form is of the type FormDataEntryValue
. Since these can be anything – a string, number, date, or File – we need to add some light validation before passing them along from our frontend form to Prisma.
Let's destructure the fields from our form and make sure they're all strings:
import { PrismaClient } from "@prisma/client";
export async function action({ request }: ActionArgs) {
let db = new PrismaClient();
let formData = await request.formData();
let { date, type, text } = Object.fromEntries(formData);
if (
typeof date !== "string" ||
typeof type !== "string" ||
typeof text !== "string"
) {
throw new Error("Bad request");
}
await db.entry.create({
data: {
date: new Date(date),
type: type,
text: text,
},
});
}
Now if we make it past our validation, TypeScript knows it's working with strings, and the errors go away. This is a good improvement to our code because even though our React form is coded to only ever accept strings, our action
is actually an API endpoint that's exposed to the public internet, meaning any client (not just our Remix app) can make a request to it. Covering all possible cases in our actions ensures our Remix server will behave as expected no matter what data comes into it.
We now have built our first complete fullstack feature: rendering a form, accepting user data, and persisting that data to our backend! Next, we'll polish up the UX of our entry form a bit.
Links
Buy Ship an app with Remix
Buy the course
Get everything in Ship an app with Remix.
- 5+ hours of video
- 19 lessons
- Private Discord
- Summaries with code
- Unlimited access to course materials
Lifetime membership
Get lifetime access to every Build UI course, including Ship an app with Remix, forever.
- Access to all five Build UI courses
- Full access to all future Build UI courses
- Summaries with code
- Video downloads
- Working code demos
- Private Discord
What's included
Stream or download every video
Watch every lesson directly on Build UI, or download them to watch offline at any time.
Live code demos
Access to a live demo of each lesson that runs directly in your browser.
Private Discord
Chat with Sam, Ryan and other Build UI members about the lessons – or anything else you're working on – in our private server.
Video summaries with code snippets
Quickly reference a lesson's material with text summaries and copyable code snippets.
Source code
Each lesson comes with a GitHub repo that includes a diff of the source code.
Invoices and receipts
Get reimbursed from your employer for becoming a better coder!