Course

Building A Server Action: Read a Product

In the previous lesson, we learned how to write a server action that creates a new Product in the database. In this lesson, we will learn how to read a Product from the database using the Prisma Client.

Creating a New Server Action - Read a Product

Let's start by creating a new server action that will allow us to read a Product in the database. Open up our products.ts in the directory.

Let's add a new function to the end of the file called that will allow us to read a Product from the database.

products.ts;

export async function getProductById() {
  // read a Product from the database
}

Before we can read a Product from the database, we need to know how to identify a Product. In this case, we will identify a Product by its . We can use the of a Product to read the Product from the database.

How do we know what data we need to provide to read a Product from the database? We can look at the Prisma schema to see what fields we can use to identify a Product.

schema.prisma
model Product {
  id          Int      @id @default(autoincrement())
  name        String
  price       Float
  category    String
  description String
  images      Image[]
  reviews     Review[]
}

The field is marked with the attribute, which means it is the primary key of the Product model. We can use the field to identify a Product in the database.

Any other fields we could use to identify a Product? We could use the field to identify a Product, but it's not unique. We could use the field to identify a Product, but it's not unique either. The field is the best choice to identify a Product in the database.

export async function getProductById(id: number) {
  // read a Product from the database
}

Now that we know how to identify a Product in the database, we can use the field to read a Product from the database. We can use the method provided by the Prisma Client to read a Product from the database.

products.ts;
("use server");

export async function getProductById(id: number) {
  const product = await prisma.product.findUnique({
    // identify the Product by its id
    where: { id },
  });
  return product;
}

Just like with the method, the method takes an object as an argument.

But this time, the object contains a property that specifies how to identify the Product we want to read from the database. In this case, we use the field to identify the Product.

Insight

Does "Where" sound familiar? It should! It's a common pattern in databases to use a clause to filter records in a table. The property in the Prisma Client is similar to the clause in SQL.

Error Handling

When working with databases, it's important to handle errors that may occur during database operations. We can use blocks to catch any errors that may occur during database operations and handle them appropriately.

products.ts;
("use server");

export async function getProductById(id: number) {
  try {
    const product = await prisma.product.findUnique({
      where: { id },
    });
    return product;
  } catch (error) {
    return null;
  }
}

Integrating the Server Action

Now that we have created the server action, let's integrate it into our application. Open up the file to see the component that displays the Product details.

Currently, the Product component just displays some placeholder data. We want to replace that placeholder data with the actual Product data from the database.

Let's pass in a "product" to the Product component. We'll fetch the Product from the database using the server action and pass it to the Product component.

Product.tsx
// import the product type from Prisma
import { Product } from "@prisma/client";

import Stars from "@/components/product/Stars";
import ImageDisplay from "@/components/product/ImageDisplay";

// Allow the Product component to accept a product as a prop
export default async function ProductView({ product }: { product: Product }) {
  if (!product) {
    return <div>Product not found</div>;
  }
  return (
    <div className="grid gap-6">
      <ImageDisplay />
      <div className="grid gap-2">
        <h1 className="text-3xl font-bold">{product.name}</h1>
        <p className="text-gray-500 dark:text-gray-400">
          {product.description}
        </p>
        <div className="flex items-center gap-4">
          <span className="text-4xl font-bold">${product.price}</span>
          <div className="flex items-center gap-0.5">
            <Stars rating={4} />
          </div>
        </div>
      </div>
    </div>
  );
}

Next we'll need to pass that Product to the Product component. Open up the file to see how the Product component is used.

We're already pulling in the params and grabbing the 'id', so let's use that ID to fetch the Product from the database and pass it to the Product component.

//
import { getProductById } from "@/lib/actions/products";

export default async function Page({ params }: { params: { path: string[] } }) {
  ...

  const product = await getProductById(parseInt(id));

  if (!product) {
    return <div>Product not found</div>;
  }
  return (
    <div className="...">
      <Product product={product} />
      ...
  );
}
Insight

Why are we using ?

The ID is coming from the URL, which is a string. We need to convert it to a number to pass it to the server action, which expects a number.

Insight

This is a common pattern. We'll often need to pass data from the URL to a component. In this case, we're passing the Product ID from the URL to the Product component.

We want to be able to navigate to different Product pages based on the Product ID in the URL. By passing the Product ID from the URL to the Product component, we can fetch the Product from the database and display its details.

Getting the rest of the Information

You might have noticed we're not filling out everything in the Product.tsx file. The component isn't getting our images. And the component has been left untouched. We'll need to fetch the rest of the information from the database to fill out the Product details.

Why do we have to fetch additional information? Why doesn't it just come with our query for the product in ?

The reason is that the Product model has relationships with other models. For example, the Product model has a one-to-many relationship with the Image model. This means that a Product can have multiple images associated with it.

When we fetch a Product from the database, we only get the fields from the Product model. We don't get the related images or reviews. We need to fetch the related images and reviews separately.

Let's update the server action to fetch the related images and reviews for the Product.

products.ts;

export async function getProductById(id: number) {
  try {
    const product = await prisma.product.findUnique({
      where: { id },
      include: {
        images: true,
        reviews: true,
      },
    });
    return product;
  } catch (error) {
    return null;
  }
}
Insight

The property allows us to specify which related models we want to include in the query. In this case, we include the and models.

This changes the data we get back from our server action from this:

//
{
  id: 1,
  name: "Product 1",
  price: 10.99,
  category: "Category 1",
  description: "Description 1",
}

To this:

//
{
  id: 1,
  name: "Product 1",
  price: 10.99,
  category: "Category 1",
  description: "Description 1",
  images: [
    {
      id: 1,
      url: "https://example.com/image1.jpg",
    },
    {
      id: 2,
      url: "https://example.com/image2.jpg",
    },
  ],
  reviews: [
    {
      id: 1,
      rating: 5,
      comment: "Great product!",
    },
    {
      id: 2,
      rating: 4,
      comment: "Good product!",
    },
  ],
}

Now that we're fetching the related images and reviews for the Product, we can update the Product component to display the images and reviews.

First, we'll change our type to include the images and reviews. Remember we can import the types from Prisma.

Product.tsx

// extend the Product type to include images and reviews
export interface ProductViewProps extends Product {
  reviews: Review[];
  images: Image[];
}

export default async function ProductView({
  product,
}: {
  product: ProductViewProps;
}) {
...
}

Next, we'll update the Product component to calculate the average rating from the reviews and display pass the images to our ImageDisplay component.

Product.tsx
export default async function ProductView({
  product,
}: {
  product: ProductViewProps;
}) {
  if (!product) {
    return <div>Product not found</div>;
  }

  // Calculate the average score of the product
  const totalScore = product.reviews.reduce(
    (acc, review) => acc + review.rating,
    0
  );
  const averageScore = Math.floor(totalScore / product.reviews.length);

  // Get the image URLs
  const imageUrls = product.images.map((image) => image.url);
  return (
    <div className="grid gap-6">
      <ImageDisplay images={imageUrls} />
      <div className="grid gap-2">
        <h1 className="text-3xl font-bold">{product.name}</h1>
        <p className="text-gray-500 dark:text-gray-400">
          {product.description}
        </p>
        <div className="flex items-center gap-4">
          <span className="text-4xl font-bold">${product.price}</span>
          <div className="flex items-center gap-0.5">
            <Stars rating={averageScore} />
          </div>
        </div>
      </div>
    </div>
  );
}

We'll now need to modify the component to accept and iterate over the images we're passing in from the database.

monkey
ImageDisplay.tsx

// modify the props to accept an array of strings
export default function ImageDisplay({ images }: { images: string[] }) {
return (
...

<CarouselContent>
  {/* map over the images */}
  {images.map((image, index) => (
    <CarouselItem key={index}>
      <img src={image} alt="product" className="h-full w-full object-cover" />
    </CarouselItem>
  ))}
</CarouselContent>
... ); }

Next, let's go to our file and update the review list to instead use the reviews from the database.

Remember, our variable now has images and reviews. So we can map over them instead of the placeholder reviews.

page.tsx
export default async function Page({ params }: { params: { path: string[] } }) {
  ...
  const product = await getProductById(parseInt(id));
  ...
  return (
    <div className="pt-20 grid md:grid-cols-2 gap-8 max-w-6xl mx-auto py-12 px-4">
      <Product product={product} />
      <div className="flex flex-col gap-y-5">
        <span className="text-2xl font-bold h-fit">Customer Reviews</span>
        <div className="grid gap-5">
          {/*
            map over reviews and send the review
            from our database to the Review component
          */}
          {product.reviews.map((review) => (
            <Review key={review.id} review={review} />
          ))}
        </div>
      </div>
      <div className="md:col-span-2">
        <AddReview id={id} />
      </div>
    </div>
  );
}

Now that we're passing reviews to our component, we can update the component to use the review data from the database.

Open up the file and update the component to use the review data from the database.

index.tsx
...
// import the type from the Prisma client
import { Review } from "@prisma/client";
...
// Update our Review component to use the review data from the database
export default function ReviewView({ review }: { review: Review }) {
  // grab the initials from the name
  const initials = review.name
    .split(" ")
    .map((n) => n[0])
    .join("");

  return (
    <Card>
      <CardContent className="grid gap-4 p-4">
        <div className="flex items-center gap-4">
          <Avatar>
            <AvatarImage alt="@jaredpalmer" src="/placeholder-avatar.jpg" />
            <AvatarFallback>{initials}</AvatarFallback>
          </Avatar>
          <div>
            <h3 className="font-semibold">{review.name}</h3>
            <div className="flex items-center gap-0.5">
              <Stars rating={review.rating} />
            </div>
          </div>
        </div>
        <p>{review.content}</p>
      </CardContent>
    </Card>
  );
}

Conclusion

In this lesson, we learned how to read a Product from the database using the Prisma Client. We created a new server action called that reads a Product from the database using the method.

You've learned to include related models in your queries using the property. This allows you to fetch related data along with the main model.

We updated the Product component to display the images and reviews associated with the Product. We also updated the Review component to use the review data from the database.

0 Comments

"Please login to view comments"

glass-bbok

Join the Conversation!

Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.

Upgrade your account
tick-guideNext Lesson

Building A Server Action: Update a Product