Build a simple E-commerce PIM with Next.js, Prisma, and Neon

Chidi Eze - Mar 9 - - Dev Community

This article will walk you through the steps of creating an e-commerce product information management system (PIM) with the Next.js framework, a Neon Postgres database, and Prisma as the object-relational mapper (ORM).

Next.js is a React framework for building fast and user-friendly full-stack applications. It provides numerous features and tools for building simple and complex web pages.

Prisma is a server-side library that helps developers read and write data to the database intuitively, efficiently, and safely.

Neon is a fully managed serverless Postgres with a generous free tier that offers a scalable and cost-effective solution for data storage, and modern developer features such as branching and bottomless storage.

This article won’t cover cart, checkout, and deployment features but will focus on how to create, read, update, and delete (CRUD) products in the database.

Here is a completed demo of this project.

Prerequisites

You’ll need the following to complete this tutorial:

  • Node.js installed on your computer.
  • Basic knowledge of React and Next.js
  • A GitHub account for (OAUTH) authentication

Getting started

We'll start with installing the Next.js package and other dependencies we'll use for this project. Open the terminal and run the below command to create a next.js starter project.

npx create-next-app neon-ecommerce
Enter fullscreen mode Exit fullscreen mode

The above command will prompt a few questions; choose yes when asked whether to use Tailwind CSS. A new project with the appropriate configurations will be created in the selected directory; in our case, it is called neon-e-commerce.

Next, navigate to the project directory and install @prisma/client, next-auth, and Prisma npm packages with the following command:

cd neon-ecommerce 
npm i @prisma/client next-auth && npm i prisma --save-dev
Enter fullscreen mode Exit fullscreen mode

Next, run the following command to launch the app:

npm run dev
Enter fullscreen mode Exit fullscreen mode

Next.js will start a live development server at http://localhost:3000.

Setting up a Neon database

Log in or create a Neon account; Neon provides one free tier account with a limit of one project per account.

Create a project

Click the “Create project” button and copy the database URL provided by Neon on the next page; this is all we need to connect the database to our project.

Our Neon dashboard

Setting up GitHub authentication

Since we'll be using GitHub for authentication, we need to register a new OAuth app on GitHub. This authentication is for admin users who want to create and sell their products, not for customers who wish to buy.

First, sign in to GitHub and go to Settings > Developer Settings > OAuth Apps.

Next, click on the "New OAuth App" button and fill out the form like in the image below:

Registering OAuth app

Learn more about next-auth oauth providers (e.g., Twitter, Google, GitHub, etc.).

After registering the application, copy the client ID and generate a new client secret.

Showing client ID and Client secret

Next, in the root directory of the project, create a .env file and set up environment variables as seen below:

    #.env
    DATABASE_URL="<YOUR NEON DATABASE URL>"
    GITHUB_ID="<YOUR GITHUB CLIENT ID>"
    GITHUB_SECRET="<THE GENERATED CLIENT SECRET>"
    NEXTAUTH_URL="http://localhost:3000/api/auth"
    NEXTAUTH_SECRET="<RUN *openssl rand -base64 32* TO GENERATE A SECRET>"
Enter fullscreen mode Exit fullscreen mode

Learn more about the next-auth secret and how it works.

Now that we've completed the setup and installation, let's start developing the application.

Building the application

The first thing we'll do is create a component folder in the root of our application and a Header.js file with the following snippet:

    //component/Header.js
    import React from "react";
    import Link from "next/link";
    import { useRouter } from "next/router";
    import { signOut, useSession } from "next-auth/react";

    const Header = () => {
      const router = useRouter();
      const isActive = (pathname) => router.pathname === pathname;
      const { data: session } = useSession();

     // #When there is NO User
    let left = (
        <div className="left">
          <Link legacyBehavior href="/">
            <a className="bold" data-active={isActive("/")}>
              Neon Ecommerce Shop
            </a>
          </Link>

        </div>
      );

      let right = null;

      if (!session) {
        right = (
          <div className="right">
            <Link legacyBehavior href="/api/auth/signin">
              <a data-active={isActive("/signup")}>Log in</a>
            </Link>

          </div>
        );
      }

      }
      return (
        <nav>
          {left}
          {right}
        </nav>
      );
    };
    export default Header;
Enter fullscreen mode Exit fullscreen mode

In the snippets above, we:

  • Imported the useRouter hook from next/router to handle navigating to different routes.
  • Imported useSession from next-auth to show our left and right nav when no user exists.

Next, let’s handle our nav for when there is a logged-in user; add the following snippets to the Header.js file:

    // When there is a user
    if (session) {
        left = (
          <div className="left">
            <Link legacyBehavior href="/">
              <a className="bold" data-active={isActive("/")}>
                Products
              </a>
            </Link>
            <Link legacyBehavior href="/p/own">
              <a data-active={isActive("/p/own")}>My Products</a>
            </Link>
          </div>
        );
        right = (
          <div className="right">
            <p>
              {session.user.name} ({session.user.email})
            </p>
            <Link legacyBehavior href="/p/create">
              <button>
                <a>New Product</a>
              </button>
            </Link>
            <button onClick={() => signOut()}>
              <a>Log out</a>
            </button>

          </div>
        );
Enter fullscreen mode Exit fullscreen mode

When a user is logged in, we display the Product, My Products, New Product, and signOut navigations.

Next, let’s add a Layout.js file within the component folder, which will manage our page layouts and help us render the Header.js component throughout the application pages.

    //component/Header.js
    import React from "react";
    import Header from "./Header";
    const Layout = (props) => (
      <div>
        <Header />
        <div >{props.children}</div>
      </div>
    );
    export default Layout;
Enter fullscreen mode Exit fullscreen mode

Next, import the Layout.js component inside the pages/index.js file and render it to see how it looks.

    //pages/index.js
    import React from "react";
    import Layout from "../components/Layout";
    const Products = () => {
      return (
        <Layout>
          <div>Hello from Homepage</div>
        </Layout>
      );
    };
    export default Products;
Enter fullscreen mode Exit fullscreen mode

In the browser, our application will look like the image below:

Showing the navigations on our homepage

Currently, the login button redirects us to the pages/api/auth route, which we are yet to develop.
Before we create the authentication route, let's generate our app schema, fill it with placeholder data, and read it so that the homepage looks a little more interesting.

Generating schema, creating and reading data

First, let’s create a prisma folder in the root of our project and create a schema.prisma file with the following snippets:

    //prisma/schema.prisma
    generator client {
      provider = "prisma-client-js"
    }

    datasource db {
      provider  = "postgresql"
      url       = env("DATABASE_URL") // uses connection pooling
    }

    model Product {
      id           String   @id @default(cuid())
      name         String?
      image        String?
      price        String?
      publish      Boolean  @default(true)
      productOwner Owner?   @relation(fields: [ownerId], references: [id])
      ownerId      String?
    }

    model Owner {
      id        String    @id @default(cuid())
      name      String?
      email     String?   @unique
      createdAt DateTime  @default(now()) @map(name: "created_at")
      updatedAt DateTime  @updatedAt @map(name: "updated_at")
      products  Product[]

      @@map(name: "owners")
    }
Enter fullscreen mode Exit fullscreen mode

Before running the npx prisma db push command to publish our schema, you can use the format command if you’re not confident everything was formatted correctly.

npx prisma format #formats the schema
npx prisma db push #publishes the schema
Enter fullscreen mode Exit fullscreen mode

After clicking on the table navigation in the neon console after publishing, we’ll see the Product and Owner models.

Showing the tables we published

At this stage, the Prisma studio is the fastest way to create data; to launch the studio and add data, run the following command:

npx prisma studio
Enter fullscreen mode Exit fullscreen mode

The studio will launch at http://localhost:5555/

Next, create a few mock products in the studio, each with a proper image URL for the product image. Each product must have an owner, select an existing user in the database, or create a new user record.

Fetching and reading data from database

Now that we have successfully created some product data, let's fetch those products and render them on our homepage.

We must establish a Prisma client instance to enable smooth interaction with our Neon database. In the root of our application, create a lib folder and a prisma.js file with the following snippets:

    //lib/prisma.js
    import { PrismaClient } from "@prisma/client";
    let prisma;
    if (process.env.NODE_ENV === "production") {
      prisma = new PrismaClient();
    } else {
      if (!global.prisma) {
        global.prisma = new PrismaClient();
      }
      prisma = global.prisma;
    }
    export default prisma;
Enter fullscreen mode Exit fullscreen mode

Next, let’s create a Product.js file in the component folder with the below snippets:

    //component/Product.js
    import Router from "next/router";
    import Image from "next/image";

    function Product({ product }) {
      return (
        <div onClick={() => Router.push(`/p/${product.id}`)}>
          <div>
            <img
              width={500}
              height={500}
              className="w-full"
              src={product.image}
              alt="item image"
            />
            <div>
              <div>{product.name}</div>
              <p>#{product.price}</p>
            </div>
            <div>
              <span>Add to cart</span>
            </div>
          </div>
        </div>
      );
    }
    export default Product;
Enter fullscreen mode Exit fullscreen mode

We will receive individual products in the snippets above and display their details.

Let's fetch the products from our database and show them on the homepage. Update the index.js file like below.

    //pages/index.js
    import prisma from "@/lib/prisma";
    import Layout from "@/components/Layout";
    import Product from "@/components/Product";

    export const getStaticProps = async () => {
      const products = await prisma.product.findMany({
        where: { publish: true },
        include: {
          productOwner: {
            select: { name: true, email: true },
          },
        },
      });
      return {
        props: { products },
        revalidate: 10,
      };
    };

    const Products = (props) => {
      return (
        <>
          <Layout>
            <div className="grid grid-cols-3 gap-2">
              {props.products.map((product) => (
                <Product key={product.id} product={product} />
              ))}
            </div>
          </Layout>
        </>
      );
    };
    export default Products;
Enter fullscreen mode Exit fullscreen mode

In the snippets above, we:

  • Used the prisma instance to fetch our published products, then provided them as props in the getStaticProps() function to our Products() function.
  • Looped through the products props and used our Product component to render each product.

All user product page with a log in button

Clicking on each product card directs us to the /p/${product.id} route, which doesn’t exist yet. Let's create that route first before we handle authentication.

Inside the pages directory, create a p folder and [id].js file with the following snippet:

    //pages/p/[id].js
    import Layout from "@/components/Layout";
    import Product from "@/components/Product";
    import prisma from "@/lib/prisma";

    export const getServerSideProps = async ({ params }) => {
      const product = await prisma.product.findUnique({
        where: {
          id: String(params?.id),
        },
        include: {
          productOwner: {
            select: { name: true, email: true },
          },
        },
      });
      return {
        props: product,
      };
    };
    function HandleProducts(props) {
      return (
        <Layout>
          <div className="max-w-4xl ">
            <Product product={props} />
          </div>
        </Layout>
      );
    }
    export default HandleProducts;
Enter fullscreen mode Exit fullscreen mode

In the above snippets, we:

  • Got the product id from the params and fetched the product details using the prisma instance in the getServerSideProps() function.
  • Displayed the product using our Product component inside the HandleProducts() function.

Now the /p/${product.id} route is in place! Clicking on any product card takes us to the corresponding page with the specific product information.

Authenticating users with GitHub OAuth provider

Recall that clicking the login button takes us to an uncreated api/auth route; let's create it now.

Create an auth folder and a [...nextauth].js file in the api folder located inside the pages directory using the following snippet:

    //pages/api/auth/[...nextauth].js
    import NextAuth from "next-auth";
    import GithubProvider from "next-auth/providers/github";
    export const authOptions = {
      providers: [
        GithubProvider({
          clientId: process.env.GITHUB_ID,
          clientSecret: process.env.GITHUB_SECRET,
        }),
        // ...add more providers here
      ],
      secret: process.env.NEXTAUTH_SECRET,
    };
    export default NextAuth(authOptions);
Enter fullscreen mode Exit fullscreen mode

In the above snippet, we imported GithubProvider and gave it our GitHub id and GitHub secret variables.

Now, when we click on the login button, we will be directed to a page where a "Sign in with GitHub" button is shown.

Sign in with GitHub button

We will be redirected to our application after signing in with our credentials.

Admin user products page with updated navigations

Notice that our navigation has been updated to include the logged-in user's name, MyProducts, NewProduct navigations, and a logout button.

Adding new products

Next, let's create the New Product page for authenticated users to create and publish their products. First, create a reusable form component to collect the product details. In the components directory, create a Product-Form file with the following snippet:

    //components/Product-Form.js
    import React from "react";
    function ProductForm({
      productName,
      productPrice,
      productImage,
      publish,
      setProductName,
      setProductPrice,
      setProductImage,
      setPublish,
      handleSubmit,
      handleCancel,
    }) {
      const checkHandler = (e) => {
        e.preventDefault();
        setPublish(!publish);
      };
      return (
        <>
          <div>
            <form onSubmit={handleSubmit}>
              {/* FORM INPUTS HERE */}
              </form>
          </div>
        </>
      );
    }
    export default ProductForm;
Enter fullscreen mode Exit fullscreen mode

In the snippet above:

  • We destructured the product properties in the ProductForm component function and used the publish and setPublish properties to create a checkHandler() function.
  • In the return() function, we returned a form tag and passed handleSubmit props to the form’s onSubmit function.

Next, let’s add the following snippet inside the form tag.

    //components/Product-Form.js
    <div>
    <div>
      <label htmlFor="product-name">Name</label>
      <input
        id="product-name"
        type="text"
        value={productName}
        onChange={(e) => setProductName(e.target.value)}
        placeholder="Product title"
        required
      />
    </div>
    <div>
      <label htmlFor="product-price">Price</label>
      <input
        id="product-price"
        type="text"
        value={productPrice}
        onChange={(e) => setProductPrice(e.target.value)}
        placeholder="Amount"
        required
      />
    </div>
    </div>
    <div>
    <div>
      <label htmlFor="product-image">Product URL</label>
      <input
        id="product-image"
        type="text"
        value={productImage}
        onChange={(e) => setProductImage(e.target.value)}
        placeholder="Image URL"
        required
      />
    </div>
    </div>
    <div>
    <div>
      <label>
        <input
          type="checkbox"
          checked={publish}
          onChange={checkHandler}
        />
        <span>Publish Immediately?</span>
      </label>
    </div>
    <div>
      <input type="submit" value="Submit" />
      <button onClick={handleCancel} type="button">
        Cancel
      </button>
    </div>
    </div>
Enter fullscreen mode Exit fullscreen mode

In the snippet above, we added various input fields for the product data, a check box to determine whether the product should be published immediately, a button to initiate the submit action, and another to cancel the process.

To show the product form in the frontend, create a create.js file inside the p directory using the following snippet:

    // pages/p/create
    import React, { useState } from "react";
    import Layout from "@/components/Layout";
    import Router from "next/router";
    import ProductForm from "@/components/Product-Form";

    const CreateProduct = () => {
      const [productName, setProductName] = useState("");
      const [productPrice, setProductPrice] = useState("");
      const [productImage, setProductImage] = useState("");
      const [publish, setPublish] = useState(false);

      const create = async (e) => {
        e.preventDefault();
        try {
          const body = { productName, productPrice, productImage, publish };
          await fetch("/api/product/create", {
            method: "POST",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
          });
          await Router.push("/p/own");
        } catch (error) {
          console.error(error);
        }
      };
      return (
        <Layout>
          <ProductForm
            handleSubmit={(e) => create(e)}
            publish={publish}
            setPublish={setPublish}
            productImage={productImage}
            setProductImage={setProductImage}
            productName={productName}
            setProductName={setProductName}
            productPrice={productPrice}
            setProductPrice={setProductPrice}
            handleCancel={() => Router.push("/p/own")}
          />
        </Layout>
      );
    };
    export default CreateProduct;
Enter fullscreen mode Exit fullscreen mode

In the snippet above:

  • We imported the ProductForm component and defined state variables to hold our product data when a user enters it in the form.
  • We also wrote a create() function, which collects all product data and sends it as a POST request to the /api/product/create route, which we'll create shortly.
  • In the return() function, we rendered the ProductForm and passed the state constants and the create() function to it.

When we navigate to the New Product page, we’ll see the form rendered like below.

New product form

Now, let's create the route we're sending the product data to; in the api folder, create a product folder, a create folder, and an index.js file with the following snippet:

    //api/product/create/index.js
    import { getServerSession } from "next-auth/next";
    import { authOptions } from "../../auth/[...nextauth]";
    import prisma from "@/lib/prisma";

    export default async (req, res) => {
      const session = await getServerSession(req, res, authOptions);
      const { productName, productPrice, productImage, publish } = req.body;
      if (session) {
        const result = await prisma.product.create({
          data: {
            name: productName,
            price: productPrice,
            image: productImage,
            publish: publish,
            productOwner: {
              connectOrCreate: {
                where: {
                  email: session.user.email,
                },
                create: {
                  email: session.user.email,
                  name: session.user.name,
                },
              },
            },
          },
        });
        res.json(result);
      } else {
        // Not Signed in
        res.status(401);
      }
      res.end();
    };
Enter fullscreen mode Exit fullscreen mode

In the snippet above:

  • We created a session constant with the getSeverSession hook and destructured productName, productPrice, productImage, and publish from the request body.

  • We checked to see if the user making the request has an active session, and if so, we called the prisma.product.create() method and gave our data, which included the productName, productPrice, productImage, and publish.

Because each product will have an owner, the connectOrCreate method determines whether the authorized user attempting to create the product has a record in the database and connects the product if a record exists or creates a new user if none exists.

Updating a product

Since users can now successfully create products, there may be a need to edit the product details. Let's implement the update functionality to allow users to update their products.

In the p directory within pages, update the [id].js file like in the below:

    //pages/p/[id].js
    // OTHER IMPORTS HERE
    import ProductForm from "@/components/Product-Form";
    // getServerSideProps() HERE
    function HandleProducts(props) {

      const [isVisible, setIsVisible] = useState(false);
      const [productName, setProductName] = useState("");
      const [productPrice, setProductPrice] = useState("");
      const [productImage, setProductImage] = useState("");
      const [published, setPublished] = useState(false);

      const { id, publish } = props;

      const userHasValidSession = Boolean(session);
      const productBelongsToUser = session?.user?.email === props.productOwner?.email;

      const updateProduct = async () => {
        try {
          const body = { productName, productPrice, productImage, published };
          await fetch(`/api/product/update/${id}`, {
            method: "PUT",
            headers: { "Content-Type": "application/json" },
            body: JSON.stringify(body),
          });
          await Router.push("/p/own");
        } catch (error) {
          console.error(error);
        }
      };
      return (
        <Layout>
          <div>
            <div>
              {/* PRODUCT COMPONENT HERE */}
              {userHasValidSession && productBelongsToUser && (
                <div className="inline-flex mt-5 py-3">
                  <button
                    onClick={() => setIsVisible(true)}
                  >
                    Edit
                  </button>
                </div>
              )}
            </div>
            <div>
              {isVisible && (
                <ProductForm
                  handleSubmit={() => updateProduct()}
                  publish={published}
                  setPublish={setPublished}
                  productImage={productImage}
                  setProductImage={setProductImage}
                  productName={productName}
                  setProductName={setProductName}
                  productPrice={productPrice}
                  setProductPrice={setProductPrice}
                  handleCancel={() => setIsVisible(false)}
                />
              )}
            </div>
          </div>
        </Layout>
      );
    }
    export default HandleProducts;
Enter fullscreen mode Exit fullscreen mode

In the snippet above, we:

  • Imported the ProductForm component and defined state constants to keep the new product data as they are entered.
  • Extracted the ID and publish from props returned by the getServerSideProps() function.
  • Created the userHasValidSession and productBelongsToUser constants to determine whether the user has an active session and whether the product belongs to the user.
  • The updateProduct() function will bundle the updated product details and send them as a PUT request to the api/product/update/${id} route we'll create soon.
  • Showed the Edit button in the return() function only if the user has an active session and owns the product; the Edit button sets the isVisible state constant to true.
  • Displayed the ProductForm component only if the isVisible value is true, and we passed updateProduct() to the form.

Next, let’s create the api/product/update/${id} route with the following snippet:

    //api/product/update/[id].js
      import prisma from "@/lib/prisma";

      export default async function handleUpdate(req, res) {

      const productId = req.query.id;
      const { productName, productPrice, productImage, publish } = req.body;

      if (req.method === "PUT") {
        const product = await prisma.product.update({
          where: { id: productId },
          data: {
            name: productName,
            price: productPrice,
            image: productImage,
            publish: publish,
          },
        });
        res.json(product);
      } else {
        throw new Error(
          `Updating product with ID: ${productId} was not sucessful`
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

In the snippet above:

  • We imported the prisma instance, set productId constant equal to req.query.id, and destructured the product properties from the request body.
  • We used the prisma instance to update the product, passing the productId as the id and the product properties as the data.

Now, users can successfully create and update products. But what if they want to publish or unpublish a product without performing a full update? Let's add the publish and unpublish functions next.

Adding publish and unpublish functions

If the product is unpublished, we want to show a publish button; otherwise, we want to offer an unpublish button. Update the [id].js file inside the p directory with the following snippet:

    //pages/p/[id].js
    // IMPORTS HERE
    // getServerSideProps() FUNCTION HERE
    function HandleProducts(props) {
      //  STATE CONSTANTS HERE
      const { id, publish } = props;
      const userHasValidSession = Boolean(session);
      const postBelongsToUser = session?.user?.email === props.productOwner?.email;

      async function handlePublish(id) {
        const body = { publish };
        await fetch(`/api/product/publish/${id}`, {
          method: "PUT",
          headers: { "Content-Type": "application/json" },
          body: JSON.stringify(body),
        });
        await Router.push("/p/own");
      }
      //   updateProduct() FUNCTION HERE
      return (
        <Layout>
          <div>
            <div>
              {/* PRODUCT COMPONENT HERE  */}
              {userHasValidSession &&
                postBelongsToUser &&
                (!props.publish ? (
                  <button onClick={() => handlePublish(id)}>Publish</button>
                ) : (
                  <button onClick={() => handlePublish(id)}>Unpublish</button>
                ))}
              {/* EDIT BUTTON HERE */}
            </div>
            {/* PRODUCTFORM COMPONENT HERE */}
          </div>
        </Layout>
      );
    }
    export default HandleProducts;
Enter fullscreen mode Exit fullscreen mode

Here:

  • We defined the handlePublish() function, saved the product's current publish status as the body constant, and sent it to the '/api/product/publish/${id}' route we will define shortly.
  • The buttons were conditionally rendered after determining whether the user had an active session and whether the product about to be published or unpublished belonged to the stated user.

Next, let’s create the '/api/product/publish/${id}' route with the following snippet:

    //api/product/publish/
    import prisma from "@/lib/prisma";

      export default async function handleActive(req, res) {
      const productId = req.query.id;
      const { publish } = req.body;
      const product = await prisma.product.update({
        where: { id: productId },
        data: { publish: !publish },
      });
      res.json(product);
    }
Enter fullscreen mode Exit fullscreen mode

Here:

  • We imported our prisma instance and destructured the publish constant from the request body.
  • We updated the product using the prisma instance and changed the publish data to the reverse of what it is.

Now, users can create, update, publish, and unpublish a product; sometimes, a user may want to delete a product completely. Let’s add the delete function.

Deleting a product

    //pages/p/[id].js

    // IMPORTS HERE
    // getServerSideProps() FUNCTION HERE
    //Delete Product
    async function deleteProduct(id) {
      if (confirm("Delete this product?")) {
        await fetch(`/api/product/delete/${id}`, {
          method: "DELETE",
        });
        Router.push("/p/own");
      } else {
        Router.push(`/p/${id}`);
      }
    }
    function HandleProducts(props) {
      // STATE CONSTANTS HERE
      const { id, publish } = props;
      const userHasValidSession = Boolean(session);
      const postBelongsToUser = session?.user?.email === props.productOwner?.email;
      //  handlePublish() FUNCTION HERE
      //  updateProduct() FUNCTION HERE
      return (
        <Layout>
          <div>
            <div>
              {/* PRODUCT COMPONENT HERE  */}
              {/* PUBLISH AND UNPUBLISH BUTTONS HERE  */}
              {userHasValidSession && postBelongsToUser && (
                <div className="inline-flex mt-5 py-3">
                  <button onClick={() => deleteProduct(id)}>Delete</button>
                  {/* Edit BUTTON HERE  */}
                </div>
              )}
            </div>
            {/* ProductForm COMPONENT HERE  */}
          </div>
        </Layout>
      );
    }
    export default HandleProducts;
Enter fullscreen mode Exit fullscreen mode

In the above snippet:

  • We constructed the deleteProduct() function, which takes the id of the product we want to delete and sends it to the /api/product/delete/${id} route we'll develop shortly.
  • We added a Delete button before the Edit button, called the deleteProduct() function, and supplied the product's id we wanted to delete.

The complete snippet of the pages/p/[id].js can be found in this GitHub gist.

Next, let’s implement the /api/product/delete/${id} route with the following snippet:

    import prisma from "@/lib/prisma";
    export default async function handleDelete(req, res) {
      const productId = req.query.id;
      if (req.method === "DELETE") {
        const product = await prisma.product.delete({
          where: { id: productId },
        });
        res.json(product);
      } else {
        throw new Error(
          `The HTTP ${req.method} method is not supported at this route.`
        );
      }
    }
Enter fullscreen mode Exit fullscreen mode

Here, we:

  • We imported our prisma instance and created a productId, which we set equal to the request query ID.
  • The prisma sample was used to delete the product, with the productId as the data.

A user who has successfully authenticated can now add new products and update, publish, and delete any product that belongs to them.

Single product page where the product belongs to the user

Conclusion

Congratulations! You successfully built a full-stack application with CRUD functionalities with Next.js, Neon, Prisma, and Next-Auth. Combining these tools can help developers create full-stack applications that are efficient and scalable. Next.js provides a structured development framework, Prisma streamlines database interactions, and Neon delivers a cost-effective and scalable database solution. The source code for this project is in this GitHub repository.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .