Saving new entries

Saving new entries

Set up Prisma and SQLite to persist our dynamic data.

Video resources

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 the DateTime 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

$179one-time payment

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

$249
access all coursesone-time payment

Lifetime access to all current and future premium Build UI courses, forever.

  • Access to our entire course library
  • New videos added regularly
  • Refactoring videos on React
  • Private Discord
  • Summaries with code
  • Full access to all future Build UI courses

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!