Skip to main content

Welcome to a Grasp lesson

Create a free account to keep studying and create your own study path.

Lesson 56

Node.js Backend: Database Integration & Data Modeling

Recommended resources

Hello! Welcome back to our course on Shopify development.

In our last lesson, we mastered using the Shopify Admin API to interact with Shopify's native data resources like products and orders. This is a powerful skill, but it only covers half of the data management story. Most non-trivial apps need to store their own information—data that doesn't fit into Shopify's predefined structure.

This brings us to a fundamental question you've been curious about: where does an app's unique data live? This lesson tackles that question head-on. We're moving beyond Shopify's data model to build our app's own persistence layer. This is a critical step in building complex, feature-rich applications and is central to our capstone project.

By the end of this lesson, you will be able to model and store app-specific data in a database connected to your Node.js backend.

The "Why": A Dedicated Database vs. Metafields

First, let's establish the "why." Shopify provides a mechanism called Metafields for adding custom data to its existing objects (like Products, Customers, or Orders). So, when should you use Metafields, and when do you need your own database?

Feature Metafields Custom Database
Purpose Extend existing Shopify objects. Store standalone, app-specific data.
Data Structure Simple key-value pairs attached to a Shopify resource. Complex, relational data models with tables, columns, and relationships.
Use Case Adding an "Assembly Time" to a Product, or a "Loyalty ID" to a Customer. Storing a merchant's configuration settings for your app, logging app usage analytics, or managing user-generated content like reviews.
Querying Limited to the context of the parent object. Powerful, flexible querying across entire datasets (e.g., "show me all merchants who enabled feature X").
Example for Capstone Storing a single customization value on an order's line item. Storing the definitions of customization options that a merchant creates (e.g., "Engraving," "Gift Wrap").

For our upcoming capstone, the "Product Customizer App," merchants will need to define sets of customization options (like text fields, color swatches) and apply them to products. This relational data—a CustomizationSet that has many CustomizationOptions—is a perfect candidate for a dedicated database.


The Toolchain: PostgreSQL and Prisma

Given your background in full-stack development, you know that connecting a Node.js backend to a database requires a few components. For this course, we'll use a professional-grade and popular stack:

  1. Database: PostgreSQL. A powerful, open-source relational database known for its reliability and robustness. While the Shopify CLI template defaults to SQLite for easy local development, PostgreSQL is a more realistic choice for production applications.
  2. ORM: Prisma. An Object-Relational Mapper (ORM) for Node.js and TypeScript. Instead of writing raw SQL, Prisma allows you to interact with your database using intuitive JavaScript/TypeScript objects. It provides excellent autocompletion and, crucially, type safety, which helps prevent bugs and improves developer productivity. Think of it as a highly intelligent translator between your application's code and the database's tables.

The "How": Implementing Data Persistence with Prisma

Let's walk through the end-to-end process of setting up Prisma, modeling our data, and using it in our Node.js/Express backend.

1. Setting Up Prisma

The Shopify app template you've been using already includes Prisma. The first step is to configure it to work with our chosen database, PostgreSQL, and understand its core components.

Learn Prisma In 60 Minutes

The video 'Learn Prisma In 60 Minutes' by Web Dev Simplified provides a fantastic, ground-up introduction to Prisma. We'll watch the first section to understand the initial setup.

Please watch from 01:05 to 06:30. This segment covers: Installing dependencies (most are already in your Shopify app template). Initializing Prisma and specifying the database provider (PostgreSQL). Understanding the two key files: .env for the database connection URL and prisma/schema.prisma for defining your data model.

As the video shows, the workflow begins with two files:

  • .env: This file holds your DATABASE_URL, which is the connection string for your PostgreSQL database. This keeps your credentials secure and separate from your code.
  • prisma/schema.prisma: This is the heart of Prisma. It is your single source of truth for your database schema. You define your data models here, and Prisma uses this file to generate migrations and the type-safe client.

2. Modeling Your Data

Now for the creative part: defining the structure of your data. We'll model the data for our capstone project's product customizer. We need a way for merchants to create a "set" of customizations and link it to a specific Shopify product.

A CustomizationSet will have a name and be associated with a Shopify product. Each CustomizationSet can contain multiple CustomizationOptions (e.g., a text input for 'Engraving', a dropdown for 'Color'). This is a classic one-to-many relationship.

The Shopify documentation for building a QR code app provides a great example of defining a new model in the schema.prisma file.

Build a Shopify app using React Router

Let's look at a concrete Shopify example from the 'Build a Shopify app' guide. This shows how to add a QRCode model to the default schema.

Read the sections 'Add the QR code data model to your database' and 'Create the table', focusing on the schema.prisma code block. Notice how the QRCode model is defined with its fields and data types.

Drawing from that example, here is how we would model our CustomizationSet and CustomizationOption in prisma/schema.prisma:

// Existing Session model from the Shopify template
model Session {
  id          String    @id
  shop        String
  state       String
  isOnline    Boolean   @default(false)
  scope       String?
  expires     DateTime?
  accessToken String
  userId      BigInt?
}

// Our new model for a set of customizations
model CustomizationSet {
  id        Int      @id @default(autoincrement())
  name      String   // e.g., "T-Shirt Options"
  productId String   // The Shopify Product GID this set applies to
  shop      String   // The shop this set belongs to
  createdAt DateTime @default(now())

  // The one-to-many relationship: One set has many options
  options   CustomizationOption[]
}

// Our new model for a single customization option
model CustomizationOption {
  id                Int      @id @default(autoincrement())
  label             String   // e.g., "Engraving Text", "Color"
  type              String   // e.g., "TEXT_INPUT", "COLOR_SWATCH"
  createdAt         DateTime @default(now())

  // The 'many' side of the relationship
  customizationSet  CustomizationSet @relation(fields: [customizationSetId], references: [id])
  customizationSetId Int
}

To fully grasp how to define relationships like the one above, the Prisma video provides an excellent explanation.

Learn Prisma In 60 Minutes

Let's return to the Prisma video to see how to officially define one-to-many, many-to-many, and one-to-one relationships.

Watch from 19:20 to 26:10. This section details how to create relationships between models. Pay close attention to the one-to-many example, as it's directly applicable to our CustomizationSet and CustomizationOption models.

3. Migrating the Database

With our models defined, we need to update our actual database to reflect this new schema. We do this with a migration. Prisma's migration tool inspects your schema.prisma file, compares it to the database's current state, and generates the necessary SQL file to sync them.

Learn Prisma In 60 Minutes

The Prisma video demonstrates this migration step clearly.

Watch the short clip from 07:54 to 09:45. It explains the prisma migrate dev command, which is the standard way to apply schema changes during development.

To run a migration, you execute the following command in your terminal:
npx prisma migrate dev --name add_customization_models

This command does three important things:

  1. Creates a migration file: It generates a new SQL file inside the prisma/migrations folder that contains the commands to create our new tables.
  2. Applies the migration: It runs the SQL file against your development database.
  3. Generates Prisma Client: It updates the @prisma/client package with type-safe methods corresponding to your new models.

4. Using Prisma Client for CRUD Operations

Now that the database is synced and the client is generated, we can start interacting with our data from our Node.js code. The Shopify app template provides a file (often app/db.server.js or similar) to initialize a single, reusable instance of the Prisma client. This is a best practice to avoid creating too many database connections.

// In a file like /app/db.server.js
import { PrismaClient } from "@prisma/client";

const prisma = global.prisma || new PrismaClient();

if (process.env.NODE_ENV !== "production") {
  global.prisma = prisma;
}

export default prisma;

With this prisma object, you can now perform Create, Read, Update, and Delete (CRUD) operations in your Express route handlers.

The Prisma documentation provides a clear example of creating functions that use the client to interact with the database.

How to use Prisma Postgres with Shopify

This Prisma guide for Shopify shows how to structure your backend code to read from and write to the database.

Read the section 'Create your Remix model'. Although the framework is Remix, the pattern is identical for Express. Focus on how the getNotes and createNote functions use prisma.productNote.findMany and prisma.productNote.create to interact with the database. This is the core pattern for all your database interactions.

Here’s how you would apply this pattern to our CustomizationSet model in an Express route:

Create a new Customization Set with Options (Nested Write)

// POST /api/customization-sets
app.post("/api/customization-sets", async (req, res) => {
  try {
    const { name, productId, shop, options } = req.body;
    
    const newSet = await prisma.customizationSet.create({
      data: {
        name,
        productId,
        shop,
        options: {
          create: options, // 'options' is an array like [{ label: 'Engraving', type: 'TEXT' }]
        },
      },
      include: {
        options: true, // Include the newly created options in the response
      },
    });

    res.status(201).json(newSet);
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
});

Read all Customization Sets for a Product

// GET /api/customization-sets?productId=...
app.get("/api/customization-sets", async (req, res) => {
  const { productId } = req.query;

  const sets = await prisma.customizationSet.findMany({
    where: {
      productId: String(productId),
    },
    include: {
      options: true, // Also fetch the related options
    },
  });

  res.status(200).json(sets);
});
Test your understanding!

Based on the patterns above, how would you write the Prisma Client query to delete a CustomizationSet with a specific id? Assume the id is available in a variable named setId.

Show answer

You would use the delete method on the corresponding model. The query would look like this:

const deletedSet = await prisma.customizationSet.delete({
  where: {
    id: setId,
  },
});

Note: Deleting a CustomizationSet might also require handling its related CustomizationOptions, depending on the cascading rules you define in your schema.

For a deep dive into the full range of CRUD operations available, the Prisma video is an invaluable resource.

Learn Prisma In 60 Minutes

To see more advanced queries and all the CRUD operations in action, let's watch the final segment from the Prisma video.

Watch from 32:24 to 45:10. This extensive section covers: Create (create, createMany), including nested creates. Read (findUnique, findFirst, findMany). Advanced filtering with where, pagination (take, skip), and ordering (orderBy). This will give you a comprehensive toolkit for querying your data.


Conclusion

Today, you've added another major component to your Shopify app development toolkit. You've learned not just how to store app-specific data, but also the principles behind when and why you need a dedicated database. We established a professional toolchain with PostgreSQL and Prisma and walked through the entire development lifecycle: modeling the data schema, migrating the database, and performing CRUD operations in your Node.js backend.

Key Takeaways:

  • Use a custom database for standalone app data, complex relationships, and powerful querying needs. Use Metafields to extend existing Shopify objects.
  • Prisma is a modern ORM that simplifies database interactions with type safety and a JavaScript-native API.
  • The development workflow with Prisma is:
    1. Model: Define your data structures in schema.prisma.
    2. Migrate: Run prisma migrate dev to sync your database with your schema.
    3. Use Client: Interact with your data in your application code using the auto-generated PrismaClient.

Next Steps

We now have an app that can read/write Shopify data and read/write to its own database. But how do we connect the two in real-time? For instance, when an order is placed on Shopify, how does our app know about it so it can process related custom data?

In the next lesson, we will explore the answer: Webhooks. You will learn how Webhooks enable real-time data synchronization, allowing your app to react instantly to events happening in a Shopify store.