Skip to main content
Prisma Documentation Docs

Search documentation

Type to search this documentation.

On this pageOverview

Shopify

Shopify is a popular platform for building e-commerce stores. This guide will show you how to connect a Shopify app to a Prisma Postgres database in order to create internal notes for products.

To start, initialize a new Shopify app using the Shopify CLI:

shopify app init

During setup, you'll be prompted to customize your app. Choose the following options:

Navigate to the prisma-store directory:

cd prisma-store

Prisma comes pre-installed in your project. In this section, you update it to Prisma ORM 7 and switch the app from SQLite to Prisma Postgres.

You will be swapping to a Prisma Postgres database, so delete the migrations folder along with the dev.sqlite file, inside of the prisma directory.

You need to update a few things in the schema.prisma file to get it working with Remix and Prisma Postgres.

  • Swap to the new prisma-client generator.
  • Update the provider to postgresql.
  • Update the url to the new database URL.
title="prisma/schema.prisma"
generator client {

  provider = "prisma-client-js"

  provider = "prisma-client"

  output   = "../app/generated/prisma"

}

datasource db {

  provider = "sqlite"

  provider = "postgresql"

  url      = "file:../dev.db"

}

model Session {

  // ... existing model

}

Create a prisma.config.ts file to configure Prisma:

title="prisma.config.ts"
import "dotenv/config"; 

import { defineConfig, env } from "prisma/config"; 

export default defineConfig({

  schema: "prisma/schema.prisma", 

  migrations: {

    path: "prisma/migrations", 

  }, 

  datasource: {

    url: env("DATABASE_URL"), 

  }, 

}); 

To enable your app to store notes for each product, add a new ProductNote model to your Prisma schema.

This model will allow you to save and organize notes linked to individual products in your database through the productGid field.

title="prisma/schema.prisma"
generator client {

  provider = "prisma-client"

  output   = "../app/generated/prisma"

}

datasource db {

  provider = "postgresql"

}

model Session {

  // ... existing model

}

model ProductNote { 

  id         String   @id @default(uuid()) 

  productGid String

  body       String?

  createdAt  DateTime @default(now()) 

  updatedAt  DateTime @updatedAt

} 

Next, Prisma will need to be updated to the latest version. Run:

bun add prisma@prev @types/pg --dev
Bash
pnpm add @prisma/client@7 @prisma/adapter-pg pg
Bash
yarn add @prisma/client@7 @prisma/adapter-pg pg
Bash
npm install @prisma/client@7 @prisma/adapter-pg pg

[!NOTE] If you are using a different database provider (MySQL, SQL Server, SQLite), install the corresponding driver adapter package instead of @prisma/adapter-pg. For more information, see Database drivers.

Initialize Prisma in your project:

bun add @prisma/client@7 @prisma/adapter-pg pg
Bash
pnpm prisma init
Bash
yarn prisma init
Bash
npx prisma init

Then create a Prisma Postgres database:

Initialize Prisma in your project:

bunx --bun prisma init
Bash
pnpm dlx create-db
Bash
yarn dlx create-db
Bash
npx create-db

Copy the connection string from the CLI output. It should look similar to this:

text
DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require"

Replace the generated DATABASE_URL in your .env file with the value from npx create-db.

Apply your database schema:

Then create a Prisma Postgres database:

bunx create-db
Bash
pnpm prisma migrate dev --name init
Bash
yarn prisma migrate dev --name init
Bash
npx prisma migrate dev --name init

Then generate Prisma Client:

Copy the connection string from the CLI output. It should look similar to this:

DATABASE_URL="postgresql://user:password@host:5432/database?sslmode=require"

Replace the generated DATABASE_URL in your .env file with the value from npx create-db.

Apply your database schema:

bunx prisma migrate dev --name init
Bash
pnpm prisma generate
Bash
yarn prisma generate
Bash
npx prisma generate

Next, update your db.server.ts file to use the newly generated Prisma client with the driver adapter:

app/db.server.ts
import { PrismaClient } from "@prisma/client";import { PrismaClient } from "./generated/prisma/client.js";import { PrismaPg } from "@prisma/adapter-pg";const adapter = new PrismaPg({  connectionString: process.env.DATABASE_URL,});if (process.env.NODE_ENV !== "production") {  if (!global.prismaGlobal) {    global.prismaGlobal = new PrismaClient();    global.prismaGlobal = new PrismaClient({ adapter });  }}const prisma = global.prismaGlobal ?? new PrismaClient();const prisma = global.prismaGlobal ?? new PrismaClient({ adapter });export default prisma;

[!WARNING] It is recommended to add app/generated/prisma to your .gitignore file.

Then generate Prisma Client:

bunx prisma generate

Next, update your db.server.ts file to use the newly generated Prisma client with the driver adapter:

title="app/db.server.ts"
import { PrismaClient } from "@prisma/client"; 

import { PrismaClient } from "./generated/prisma/client.js"; 

import { PrismaPg } from "@prisma/adapter-pg"; 

const adapter = new PrismaPg({

  connectionString: process.env.DATABASE_URL, 

}); 

if (process.env.NODE_ENV !== "production") {

  if (!global.prismaGlobal) {

    global.prismaGlobal = new PrismaClient(); 

    global.prismaGlobal = new PrismaClient({ adapter }); 

  }

}

const prisma = global.prismaGlobal ?? new PrismaClient(); 

const prisma = global.prismaGlobal ?? new PrismaClient({ adapter }); 

export default prisma;

Create a new models/ folder. Inside this folder, add a file named notes.server.js to hold the note-related database logic.

The notes.server.js file will contain two functions:

  • getNotes - This will get all the notes for a given product.
  • createNote - This will create a new note for a given product.

Start by importing the Prisma client from db.server.ts and creating the getNotes function:

title="models/notes.server.js"
import prisma from "../db.server"; 

export const getNotes = async (productGid) => {

  const notes = await prisma.productNote.findMany({

    where: { productGid: productGid.toString() }, 

    orderBy: { createdAt: "desc" }, 

  }); 

  return notes; 

}; 

To enable users to add new notes to your database, create a function in notes.server.js that uses prisma.productNote.create:

title="models/notes.server.js"
import prisma from "../db.server";

export const getNotes = async (productGid) => {

  const notes = await prisma.productNote.findMany({

    where: { productGid: productGid.toString() },

    orderBy: { createdAt: "desc" },

  });

  return notes;

};

export const createNote = async (note) => {

  const newNote = await prisma.productNote.create({

    data: {

      body: note.body, 

      productGid: note.productGid, 

    }, 

  }); 

  return newNote; 

}; 

Before those functions are able to be called, our route needs a layout to sit in. This layout route will feature a button for selecting a product, and will act as the parent for your ProductNotes route.

Start by creating the folder routes/app.product-notes.jsx and adding the ProductNotesLayout component inside of it:

title="app/routes/app.product-notes.jsx"
import { Page, Layout } from "@shopify/polaris"; export default function ProductNotesLayout() {  return (    <Page title="Product Notes">      {" "}      <Layout>        {" "}        <Layout.Section></Layout.Section>      </Layout>{" "}    </Page>   ); } 

Next, create the selectProduct function and a Button to let the user pick a product:

title="app/routes/app.product-notes.jsx"
import { useNavigate } from "@remix-run/react";

import { Page, Layout } from "@shopify/polaris"; 

import { Button, Page, Layout } from "@shopify/polaris"; 

export default function ProductNotesLayout() {

  const navigate = useNavigate(); 

  async function selectProduct() {

    const products = await window.shopify.resourcePicker({

      type: "product", 

      action: "select", 

    }); 

    const selectedGid = products[0].id; 

    navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`); 

  } 

  return (

    <Page title="Product Notes">

      <Layout>

        <Layout.Section>

          <Button onClick={selectProduct} fullWidth size="large">

            {" "}

            // [!code ++] Select Product

          </Button>{" "}

        </Layout.Section>

      </Layout>

    </Page>

  );

}

Remix renders provides the ability to render a nested route. Add an <Outlet /> to the routes/app.product-notes.jsx file where the ProductNotes route will be rendered:

title="app/routes/app.product-notes.jsx"
import { useNavigate } from "@remix-run/react"; import { Outlet, useNavigate } from "@remix-run/react"; import { Page, Button, Layout } from "@shopify/polaris";export default function ProductNotesLayout() {  const navigate = useNavigate();  async function selectProduct() {    const products = await window.shopify.resourcePicker({      type: "product",      action: "select",    });    const selectedGid = products[0].id;    navigate(`/app/product-notes/${encodeURIComponent(selectedGid)}`);  }  return (    <Page title="Product Notes">      <Layout>        <Layout.Section>          <Button onClick={selectProduct} fullWidth size="large">            Select Product          </Button>        </Layout.Section>        <Outlet />      </Layout>    </Page>  );}

If you run npm run dev, you won't be able to see the Product Notes route. To fix this, you need to add the ProductNotesLayout to the app.jsx file so it shows up in the sidebar:

title="app/routes/app.jsx"
import { Link, Outlet, useLoaderData, useRouteError } from "@remix-run/react";

import { boundary } from "@shopify/shopify-app-remix/server";

import { AppProvider } from "@shopify/shopify-app-remix/react";

import { NavMenu } from "@shopify/app-bridge-react";

import polarisStyles from "@shopify/polaris/build/esm/styles.css?url";

import { authenticate } from "../shopify.server";

export const links = () => [{ rel: "stylesheet", href: polarisStyles }];

export const loader = async ({ request }) => {

  await authenticate.admin(request);

  return { apiKey: process.env.SHOPIFY_API_KEY || "" };

};

export default function App() {

  const { apiKey } = useLoaderData();

  return (

    <AppProvider isEmbeddedApp apiKey={apiKey}>

      <NavMenu>[ Home ](/app) [Product Notes](/app/product-notes) // [!code ++]</NavMenu>

      <Outlet />

    </AppProvider>

  );

}

// Shopify needs Remix to catch some thrown responses, so that their headers are included in the response.

export function ErrorBoundary() {

  return boundary.error(useRouteError());

}

export const headers = (headersArgs) => {

  return boundary.headers(headersArgs);

};

Currently, if you run npm run dev and navigate to the Product Notes route, you will see nothing once selecting a product.

Follow these steps to create the product notes route:

Create a new routes/app/app.notes.$productGid.jsx file which will take in the productGid as a parameter, and return the product notes associated with the product as well as a form to create a new note:

title="app/routes/app/app.notes.$productGid.jsx"
export default function ProductNotes() {

  return (

    <></> 

  ); 

} 

On load, the route will need to fetch the notes for the product and display them.

Add a loader function to the route:

title="app/routes/app/app.notes.$productGid.jsx"
import { json } from "@remix-run/node"; 

import { useLoaderData } from "@remix-run/react"; 

import { getNotes } from "../models/note.server"; 

export const loader = async ({ params }) => {

  const { productGid } = params; 

  const notes = await getNotes(productGid); 

  return json({ notes, productGid }); 

}; 

export default function ProductNotes() {

  const { notes, productGid } = useLoaderData(); 

  return <></>;

}

Map out the notes in the ProductNotes component, using Polaris components:

title="app/routes/app/app.notes.$productGid.jsx"
import { json } from "@remix-run/node";import { useLoaderData } from "@remix-run/react";import { getNotes } from "../models/note.server";import { Card, Layout, Text, BlockStack } from "@shopify/polaris"; export const loader = async ({ params }) => {  const { productGid } = params;  const notes = await getNotes(productGid);  return json({ notes, productGid });};export default function ProductNotes() {  const { notes, productGid } = useLoaderData();  return (    <>      <Layout.Section>        {" "}        <BlockStack gap="200">          {" "}          {notes.length === 0 ? (             <Text as="p" variant="bodyMd" color="subdued">              {" "}              // [!code ++] No notes yet.            </Text>           ) : (            notes.map(              (                note,               ) => (                <Card key={note.id} sectioned>                  {" "}                  <BlockStack gap="100">                    {" "}                    {note.body && (                       <Text as="p" variant="bodyMd">                        {" "}                        {note.body}                      </Text>                     )}{" "}                    <Text as="p" variant="bodySm" color="subdued">                      {" "}                      // [!code ++] Added: {new Date(note.createdAt).toLocaleString()}                    </Text>{" "}                  </BlockStack>{" "}                </Card>               ),            )           )}{" "}        </BlockStack>{" "}      </Layout.Section>{" "}    </>  );}

You should be seeing "No notes yet.". If so, you're on the right track.

A few things need to be added to the route in order to create a new note:

  • Add an action function to the route.
  • Display a Toast notification when a note is created.
  • Import the createNote function from models/note.server.js.
  • Import the useActionData and useAppBridge
title="app/routes/app/app.notes.$productGid.jsx"
import { json, redirect } from "@remix-run/node";

import { useLoaderData } from "@remix-run/react"; 

import { useLoaderData, useActionData } from "@remix-run/react"; 

import { getNotes } from "../models/note.server"; 

import { getNotes, createNote } from "../models/note.server"; 

import { Card, Layout, Text, BlockStack } from "@shopify/polaris";

import { useAppBridge } from "@shopify/app-bridge-react"; 

export const loader = async ({ params }) => {

  const { productGid } = params;

  const notes = await getNotes(productGid);

  return json({ notes, productGid });

};

export const action = async ({ request, params }) => {

  const formData = await request.formData(); 

  const body = formData.get("body")?.toString() || null; 

  const { productGid } = params; 

  await createNote({ productGid, body }); 

  return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`); 

}; 

export default function ProductNotes() {

  const { notes, productGid } = useLoaderData();

  const actionData = useActionData(); 

  const app = useAppBridge(); 

  useEffect(() => {

    if (actionData?.ok) {

      app.toast.show("Note saved", { duration: 3000 }); 

      setBody(""); 

    } 

  }, [actionData, app]); 

  return (

    <>

      <Layout.Section>

        <BlockStack gap="200">

          {notes.length === 0 ? (

            <Text as="p" variant="bodyMd" color="subdued">

              No notes yet.

            </Text>

          ) : (

            notes.map((note) => (

              <Card key={note.id} sectioned>

                <BlockStack gap="100">

                  {note.body && (

                    <Text as="p" variant="bodyMd">

                      {note.body}

                    </Text>

                  )}

                  <Text as="p" variant="bodySm" color="subdued">

                    Added: {new Date(note.createdAt).toLocaleString()}

                  </Text>

                </BlockStack>

              </Card>

            ))

          )}

        </BlockStack>

      </Layout.Section>

    </>

  );

}

Now, you can build out the form that will call the action function:

title="app/routes/app/app.notes.$productGid.jsx"
import { json, redirect } from "@remix-run/node";import { useLoaderData, useActionData } from "@remix-run/react";import { getNotes, createNote } from "../models/note.server";import { Card, Layout, Text, BlockStack } from "@shopify/polaris"; import {  Card,  Layout,  Text,  BlockStack,  Form,  FormLayout,  TextField,  Button,} from "@shopify/polaris"; import { useAppBridge } from "@shopify/app-bridge-react";export const loader = async ({ params }) => {  const { productGid } = params;  const notes = await getNotes(productGid);  return json({ notes, productGid });};export const action = async ({ request, params }) => {  const formData = await request.formData();  const body = formData.get("body")?.toString() || null;  const { productGid } = params;  await createNote({ productGid, body });  return redirect(`/app/product-notes/${encodeURIComponent(productGid)}`);};export default function ProductNotes() {  const { notes, productGid } = useLoaderData();  const actionData = useActionData();   const app = useAppBridge();   useEffect(() => {    if (actionData?.ok) {      app.toast.show("Note saved", { duration: 3000 });       setBody("");     }   }, [actionData, app]);   return (    <>      <Layout.Section>        {" "}        <Card sectioned>          {" "}          <Form method="post">            {" "}            <FormLayout>              {" "}              <BlockStack gap="200">                {" "}                <input type="hidden" name="productGid" value={productGid} />                <TextField                  label="Note"                  value={body}                   onChange={setBody}                   name="body"                  autoComplete="off"                  multiline={4}                 />{" "}                <Button submit primary>                  {" "}                  // [!code ++] Add Note                </Button>{" "}              </BlockStack>{" "}            </FormLayout>{" "}          </Form>{" "}        </Card>{" "}      </Layout.Section>{" "}      <Layout.Section>        <BlockStack gap="200">          {notes.length === 0 ? (            <Text as="p" variant="bodyMd" color="subdued">              No notes yet.            </Text>          ) : (            notes.map((note) => (              <Card key={note.id} sectioned>                <BlockStack gap="100">                  {note.body && (                    <Text as="p" variant="bodyMd">                      {note.body}                    </Text>                  )}                  <Text as="p" variant="bodySm" color="subdued">                    Added: {new Date(note.createdAt).toLocaleString()}                  </Text>                </BlockStack>              </Card>            ))          )}        </BlockStack>      </Layout.Section>    </>  );}

You should now be able to add a note to a product and see it displayed.

Run npm run dev and navigate to the Product Notes route.

  • Navigate to Product Notes on the sidebar
  • Select a product
  • Add a note
  • Verify that notes are displayed and saved correctly.

Now that you have a working Shopify app connected to a Prisma Postgres database, you can:

  • Extend your Prisma schema with more models and relationships
  • Add create/update/delete routes and forms
Suggest an edit

Propose a replacement for this page. The site team reviews it before applying any changes.

Export
Documentation menu