— Nextjs, Prisma, Fullstack — 6 min read
In this tutorial, I will show you how you can build an app with Nextjs and Prisma. By the end, you will be able to;
Note: In this tutorial, we'll use an already built UI for an e-commerce app by my friend Nader Dabit. His app didn't have a backend, so that's what we'll be adding. Let's get started.
The first thing we want to do is clone Nader's repo and install dependencies. So go ahead and run this on your terminal;
1git clone git@github.com:jamstack-cms/jamstack-ecommerce.git
Then move into the projects folder and run yarn
to install the packages. After the installation, you can verify that you can run the app by running yarn dev
. You should be able to open the application.
Next, we want to install the Prisma CLI as a dev dependency. We will do so by running yarn add prisma --dev
We will then use the newly installed Prisma CLI to instantiate some files that Prisma requires for us. We will do this by invoking the CLI by running npx prisma init
The above command will create a .env
file and a prisma
folder at the root of our project. The prisma folder will have a schema.prisma
file; we will define our schema and configure our provider here, while in the env file, we'll store our sensitive database credentials.
We will configure Prisma to use SQLite provider by defining this in the datasource block in the schema.prisma
file.
1// schema.prisma2
3datasource db {4 provider = "sqlite"5 url = "file:./db"6}7
8generator client {9 provider = "prisma-client-js"10}
The above configuration tells Prisma that we want to use SQLite and the file name is db
stored in our current directory. In this case, inside the prisma directory.
Next, we want to model the shape of our schema. Prisma provides a way to achieve this in a declarative way. But first, we need to have an idea of what tables and columns to declare. We want to have two tables; Category and Product.
I deduced this by inspecting the type of data our E-commerce app requires. At the moment, it has this data hardcoded under the utils
folder.
Since now we have an idea of the shape of our data, we can go ahead and define the two models as depicted below in our schema.prisma
file.
1model Category {2 id Int @id3 name String4 products Product[] @relation(references: [id])5 image String?6}7
8model Product {9 id Int @id10 name String11 price Float12 image String13 brand String14 categories Category[] @relation(references: [id])15 currentInventory Int16 description String17}
The above defines two models with some fields. The interesting bit is where we create relations between the two models. In the Category
model, we define a products
field which is an array of products. In the Product
model, we have a categories
field which is an array of categories.
This type of relation is a unidirectional type, where two models make references to each other. It is also a one-to-many relation for one product can appear in many categories, and one category can have many products in it.
It is also important to note that this connections do not exist in the database, they only exist at the Prisma level.
So far, we have defined our schema, and it would be nice to at least view this structure of our database. Prisma comes with a GUI that enables us to do precisely this. But first, we need to tell Prisma to update our model. To do this, we will run npx prisma db push
which will create our SQLite database with the structure we defined. This file will be stored under the prisma
folder.
Also note, since this is our first time running this command, Prisma CLI will do two additional things for us;
And with that, we can now open prisma studio by running npx prisma studio.
We can view the two tables we specified but no data in them. Let's go ahead and address that next.
Before we add some data to our database, let's first install and configure our project to use typescript. This addition is not a requisite, but I'd highly recommend it for it will make our developer experience way better and prevent us from shipping preventable bugs. Prisma client will also infer our types.
Let's add the following packages as dev dependencies;
yarn add typescript @types/node ts-node @types/react --dev
Then let's add a tsconfig.json
file in the root of our project with the following contents.
1{2 "compilerOptions": {3 "baseUrl": ".",4 "target": "es5",5 "lib": ["dom", "dom.iterable", "esnext"],6 "allowJs": true,7 "skipLibCheck": true,8 "strict": false,9 "forceConsistentCasingInFileNames": true,10 "noEmit": true,11 "esModuleInterop": true,12 "module": "commonjs",13 "moduleResolution": "node",14 "resolveJsonModule": true,15 "isolatedModules": true,16 "jsx": "preserve"17 },18 "include": ["next-env.d.ts", "**/*.ts", "**/*.tsx"],19 "exclude": ["node_modules"]20}
Then we need to add a compiler option in our scripts in our package.json file.
1"scripts": {2 ...3 "ts-node": "ts-node --compiler-options '{\"module\":\"CommonJS\"}'"4 },
This script will enable prisma db seed
to work well with Nextjs's tooling.
Since we now have that out of the way, let's create a prisma client instance that we can reuse throughout our codebase. We will create a lib folder and add a prisma-client.ts
file on there with the contents below.
1import { PrismaClient } from "@prisma/client";2
3// add prisma to the NodeJS global type4interface CustomNodeJsGlobal extends NodeJS.Global {5 prisma: PrismaClient;6}7
8// Prevent multiple instances of Prisma Client in development9declare const global: CustomNodeJsGlobal;10
11const prisma = global.prisma || new PrismaClient();12
13if (process.env.NODE_ENV === "development") global.prisma = prisma;14
15export default prisma;
Let's then create a data.ts
file under the utils
folder. This file will contain some data to import and use the prisma client to insert into our database. Please copy and paste the data below into this file.
1interface ProductInterface {2 id: number3 name: string4 categories: number[]5 price: number6 image: string7 description: string8 currentInventory: number9 brand?: string10}11
12interface CategoryInterface {13 id?: number14 image?: string15 name: string16}17
18export const categories: CategoryInterface[] = [19 {20 id: 1,21 name: "new arrivals",22 image: "/products/couch1.png",23 },24 { id: 2, name: "sofas", image: "/products/couch5.png" },25 {26 id: 3,27 name: "living room",28 image: "/products/couch5.png",29 },30 {31 id: 4,32 name: "on sale",33 image: "/products/couch8.png",34 },35 {36 id: 5,37 name: "chairs",38 image: "/products/chair1.png",39 },40]41
42export const products: ProductInterface[] = [43 {44 id: 1,45 categories: [1],46 name: "Timber Gray Sofa",47 price: 1000,48 image: "/products/couch1.png",49 description:50 "Stay a while. The Timber charme chocolat sofa is set atop an oak trim and flaunts fluffy leather back and seat cushions. Over time, this brown leather sofa’s full-aniline upholstery will develop a worn-in vintage look. Snuggle up with your cutie (animal or human) and dive into a bowl of popcorn. This sofa is really hard to leave. Natural color variations, wrinkles and creases are part of the unique characteristics of this leather. It will develop a relaxed vintage look with regular use.",51 brand: "Jason Bourne",52 currentInventory: 4,53 },54 {55 id: 2,56 categories: [2, 3],57 name: "Carmel Brown Sofa",58 price: 1000,59 image: "/products/couch5.png",60 description:61 "Stay a while. The Timber charme chocolat sofa is set atop an oak trim and flaunts fluffy leather back and seat cushions. Over time, this brown leather sofa’s full-aniline upholstery will develop a worn-in vintage look. Snuggle up with your cutie (animal or human) and dive into a bowl of popcorn. This sofa is really hard to leave. Natural color variations, wrinkles and creases are part of the unique characteristics of this leather. It will develop a relaxed vintage look with regular use.",62 brand: "Jason Bourne",63 currentInventory: 2,64 },65 {66 id: 3,67 categories: [1, 2],68 name: "Mod Leather Sofa",69 price: 800,70 image: "/products/couch6.png",71 description:72 "Easy to love. The Sven in birch ivory looks cozy and refined, like a sweater that a fancy lady wears on a coastal vacation. This ivory loveseat has a tufted bench seat, loose back pillows and bolsters, solid walnut legs, and is ready to make your apartment the adult oasis you dream of. Nestle it with plants, an ottoman, an accent chair, or 8 dogs. Your call.",73 brand: "Jason Bourne",74 currentInventory: 8,75 },76 {77 id: 4,78 categories: [1, 2],79 name: "Thetis Gray Love Seat",80 price: 900,81 image: "/products/couch7.png",82 description:83 "You know your dad’s incredible vintage bomber jacket? The Nirvana dakota tan leather sofa is that jacket, but in couch form. With super-plush down-filled cushions, a corner-blocked wooden frame, and a leather patina that only gets better with age, the Nirvana will have you looking cool and feeling peaceful every time you take a seat. Looks pretty great with a sheepskin throw, if we may say so. With use, this leather will become softer and more wrinkled and the cushions will take on a lived-in look, like your favorite leather jacket.",84 brand: "Jason Bourne",85 currentInventory: 10,86 },87 {88 id: 5,89 categories: [4, 2],90 name: "Sven Tan Matte",91 price: 1200,92 image: "/products/couch8.png",93 description:94 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",95 brand: "Jason Bourne",96 currentInventory: 7,97 },98 {99 id: 6,100 categories: [4, 2],101 name: "Otis Malt Sofa",102 price: 500,103 image: "/products/couch9.png",104 description:105 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",106 brand: "Jason Bourne",107 currentInventory: 13,108 },109 {110 id: 7,111 categories: [4, 2],112 name: "Ceni Brown 3 Seater",113 price: 650,114 image: "/products/couch10.png",115 description:116 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",117 brand: "Jason Bourne",118 currentInventory: 9,119 },120 {121 id: 8,122 categories: [2, 3],123 name: "Jameson Jack Lounger",124 price: 1230,125 image: "/products/couch11.png",126 description:127 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",128 brand: "Jason Bourne",129 currentInventory: 24,130 },131
132 {133 id: 9,134 categories: [2],135 name: "Galaxy Blue Sofa",136 price: 800,137 image: "/products/couch2.png",138 description:139 "Easy to love. The Sven in birch ivory looks cozy and refined, like a sweater that a fancy lady wears on a coastal vacation. This ivory loveseat has a tufted bench seat, loose back pillows and bolsters, solid walnut legs, and is ready to make your apartment the adult oasis you dream of. Nestle it with plants, an ottoman, an accent chair, or 8 dogs. Your call.",140 brand: "Jason Bourne",141 currentInventory: 43,142 },143 {144 id: 10,145 categories: [1, 2],146 name: "Markus Green Love Seat",147 price: 900,148 image: "/products/couch3.png",149 description:150 "You know your dad’s incredible vintage bomber jacket? The Nirvana dakota tan leather sofa is that jacket, but in couch form. With super-plush down-filled cushions, a corner-blocked wooden frame, and a leather patina that only gets better with age, the Nirvana will have you looking cool and feeling peaceful every time you take a seat. Looks pretty great with a sheepskin throw, if we may say so. With use, this leather will become softer and more wrinkled and the cushions will take on a lived-in look, like your favorite leather jacket.",151 brand: "Jason Bourne",152 currentInventory: 2,153 },154 {155 id: 11,156 categories: [4, 2],157 name: "Dabit Matte Black",158 price: 1200,159 image: "/products/couch4.png",160 description:161 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",162 brand: "Jason Bourne",163 currentInventory: 14,164 },165
166 {167 id: 12,168 categories: [4, 5],169 name: "Embrace Blue",170 price: 300,171 image: "/products/chair1.png",172 description:173 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",174 brand: "Jason Bourne",175 currentInventory: 12,176 },177 {178 id: 13,179 categories: [4, 5],180 name: "Nord Lounger",181 price: 825,182 image: "/products/chair2.png",183 description:184 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",185 brand: "Jason Bourne",186 currentInventory: 13,187 },188 {189 id: 14,190 categories: [4, 5],191 name: "Ceni Matte Oranve",192 price: 720,193 image: "/products/chair3.png",194 description:195 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",196 brand: "Jason Bourne",197 currentInventory: 33,198 },199 {200 id: 15,201 categories: [4, 5],202 name: "Abisko Green Recliner",203 price: 2000,204 image: "/products/chair4.png",205 description:206 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",207 brand: "Jason Bourne",208 currentInventory: 23,209 },210 {211 id: 16,212 categories: [4, 5],213 name: "Denim on Denim Single",214 price: 1100,215 image: "/products/chair5.png",216 description:217 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",218 brand: "Jason Bourne",219 currentInventory: 13,220 },221 {222 id: 17,223 categories: [4, 5],224 name: "Levo Tan Lounge Chair",225 price: 600,226 image: "/products/chair6.png",227 description:228 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",229 brand: "Jason Bourne",230 currentInventory: 15,231 },232
233 {234 id: 18,235 categories: [4, 5],236 name: "Anime Tint Recliner",237 price: 775,238 image: "/products/chair7.png",239 description:240 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",241 brand: "Jason Bourne",242 currentInventory: 44,243 },244 {245 id: 19,246 categories: [4, 5],247 name: "Josh Jones Red Chair",248 price: 1200,249 image: "/products/chair8.png",250 description:251 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",252 brand: "Jason Bourne",253 currentInventory: 17,254 },255 {256 id: 20,257 categories: [4, 5],258 name: "Black Sand Lounge",259 price: 1600,260 image: "/products/chair9.png",261 description:262 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",263 brand: "Jason Bourne",264 currentInventory: 28,265 },266 {267 id: 21,268 categories: [4, 5],269 name: "Mint Beige Workchair",270 price: 550,271 image: "/products/chair10.png",272 description:273 "You don’t have to go outside to be rugged. The Cigar rawhide sofa features a sturdy corner-blocked wooden frame and raw seams for that Malboro-person look. This brown leather sofa is cozy in a cottage, cabin, or a condo. And the leather (the leather!) becomes more beautiful with use: subtle character markings such as insect bites, healed scars, and grain variation reflects a real vintage. Saddle up and pass the remote.",274 brand: "Jason Bourne",275 currentInventory: 31,276 },277]
Finally, we will create a seed.ts
file under the prisma
folder. Here is where we will insert the data into our database. The files contents will be as below;
1import { categories, products } from "utils/data";2import prisma from "lib/prisma-client";3
4async function main() {5 // creates categories6 await Promise.all(7 categories.map(({ name, id, image }) =>8 prisma.category.upsert({9 where: { id: id },10 update: {},11 create: { name, id, image },12 })13 )14 );15 await Promise.all(16 products.map(17 ({18 categories,19 id,20 name,21 price,22 image,23 description,24 brand,25 currentInventory,26 }) =>27 prisma.product.upsert({28 where: { id },29 update: {},30 create: {31 id,32 name,33 price,34 image,35 description,36 brand,37 currentInventory,38 categories: {39 connect: categories.map((id) => ({ id })),40 },41 },42 })43 )44 );45}46
47main()48 .then(() => console.log(`Seeded data successfully`))49 .catch((e) => console.error(`Failed to seed data, ${e}`))50 .finally(async () => {51 await prisma.$disconnect();52 });53
54export default main;
Before we run our seeding scripts, make sure you have ts-node
installed globally. You can do this by running npm install -g ts-node
Now let's go ahead and run prisma db seed --preview-feature
. When this operation is successful, you should be able to see a success message.
I had some instances where I was getting an error when using an SQlite provider. The error message looked something like this;
1Failed to seed data, Error:2Invalid `prisma.product.upsert()` invocation:3
4 Error occurred during query execution:5ConnectorError(ConnectorError { user_facing_error: None, kind: ConnectionError(Timed out during query execution.) })
If you happen to get this error, first check if the db
file has been created and if not, hit Ctrl + C
and rerun the prisma db seed --preview-feature
command, and hopefully, you will be successful this time. I have raised this issue as a bug on the Prisma repo.
If you are successful in seeding the data, you can open Prisma Studio to view the data by running npx prisma studio.
We now have a data store and a way to interact with the its data. We can now fetch this data in our app and pass that to our UI to render it.
Since this is a Jamstack application, we will use the getStaticProps
API that Nextjs provides to build a static site. We can start by adding this in our pages/index.js
file. Let's first convert this to a typescript file by renaming it to index.tsx
file. Then we can fetch the data required on the homepage by replacing the code in the getStaticProps
method as below;
1import prismaClient from "lib/prisma-client"2...3export async function getStaticProps() {4 const categories = await prismaClient.category.findMany({5 include: {6 products: true,7 },8 })9
10 const inventoryData = await prismaClient.product.findMany({11 include: {12 categories: true,13 },14 })15
16 return {17 props: {18 inventoryData,19 categories,20 },21 revalidate: 1,22 }23}
Notice the revalidate config, which is an opt-in feature for incremental static regeneration. It enables our app to update our pages in the background whenever our data changes. This is useful for our data is dynamic and we want to show fresh data to our users.
We can also update the pages/product/[name].js
file. This file dynamically creates routes for all of our products; product slugs. It achieves this by using the getStaticPaths
API by fetching all the products that we have in our database, creates an array of slugs passing that to the paths key of the returned object.
1import { slugify } from "utils/helpers"2import prismaClient from "lib/prisma-client"3...4export async function getStaticPaths() {5 const products = await prismaClient.product.findMany();6 const paths = products.map((product) => {7 return { params: { name: slugify(product.name) } };8 });9 return {10 paths,11 fallback: false,12 };13}
Next, we also want to fetch the data associated with the current product slug. We know that the slug
will be available to the page as a param, and therefore we can use that to filter that specific product using Prisma. We can achive this by using the getStaticProps
API and using one of Prisma's filtering
API to get this specific piece of data as below;
1...2export async function getStaticProps({ params }) {3 const name = params.name.replace(/-/g, " ");4 const product = await prismaClient.product.findFirst({5 where: {6 name: {7 contains: name,8 },9 },10 });11 return {12 props: {13 product,14 },15 };16}
We then update all the pages that need any categories and product data to read from our database instead of being hard-coded.
You can try doing this yourself or see the repo with the finished code changes.
Up to this point, our Prisma config is pointing to an SQLite file which is good enough to prototype. But in real-world applications, you most likely want to use a Postgres or a Mysql database.
We can do this by adding a DATABASE_URL
variable in our .env file then referencing that in our datasource block like so;
1datasource db {2 provider = "postgresql"3 url = env("DATABASE_URL")4}
After doing this, let's delete the SQLite file in our Prisma folder. Then we run prisma db push,
which syncs our schema with the database. We can also then seed the database by running prisma db seed
Prisma also comes with a way to create migrations files for you. You can do this by running npx prisma migrate dev
. This command will create a migrations
folder in your prisma folder, which will be handy when moving your application between different environments.
We have been able to add a backend to our Jamstack E-commerce from scratch!
I hope this gives you an idea of using Prisma to model your schema, view your data using Prisma Studio and interact with your database using Prisma Client.
We have also touched on creating dynamic routes, fetch data during build time, and opt-in for static site regeneration with Nextjs.
For more and in-depth documentation, please refer to prisma.io. Similarly, please refer to Nextjs docs for more strategies that you can use to fetch data.
If you enjoyed this tutorial, please share it to reach and help other people and give me a follow on Twitter @africansinatra for more content like this.
Happy coding, cheers!