Course

Building Server Actions

Interacting with the Database

In this lesson, we will learn how to interact with the database using Mongoose using server actions.

Basic Principles

Just like with any interactive web application- we build our actions out following the basic principles of CRUD (Create, Read, Update, Delete).

: Add new data to the database

: Retrieve data from the database

: Modify existing data in the database

: Remove data from the database

If we start with these basic principles in mind, we can build out our server actions to interact with the database in a meaningful way.

Creating a New Server Action - Create a Product

Let's start by creating a new server action that will allow us to create a new Product in the database. We will create a new file called products.ts in the directory.

products.ts
"use server"

// Import the database connection
import dbConnect from '@/lib/db';

export async function createProduct() {
  // make sure the database is connected
  await dbConnect();
  // TODO: Create the product

}

To flesh out our createProduct function, we will need to import the Product model from our models/Product.ts file. We will also need to pass in the data for the new product that we want to create.

Let's think about what information we need when we create a new product. We'll need:

  • name
  • description
  • price
  • category
  • imageUrl

How do we know what data we need to pass in? We can look at the Product schema to see what fields are required.

product.ts

const ProductSchema = new Schema<Product>({
  name: String,
  price: Number,
  description: String,
  category: String,
  images: [String],
});

Now that we know what data we need to pass in, let's update our createProduct function to accept this data and create a new product in the database.

products.ts

"use server";

import Product from "@/lib/models/product";
// Import the database connection
import dbConnect from "@/lib/db";

export async function createProduct(product: Product) {
  // make sure the database is connected
  await dbConnect();
  const newProduct = await Product.create({
    name: product.name,
    price: product.price,
    description: product.description,
    category: product.category,
    images: product.images,
  });
  const id = newProduct._id.toString();
  return id;

}

What's happening here? We import from our file. We then use the method to create a new product in the database with the data that we passed in. We then return the new product that was created.

Because we named our interface and our model the same, we get access to both the type and the model in the same place. This makes it easier to work with the data and the model in the same file, that's why we can use as both the type and the model.

Can you think of a cleaner way to write this function?

Hint
monkey
//
export async function createProduct(product: Product) {
  await dbConnect();
  const newProduct = await Product.create(product);
  return newProduct._id.toString();

}

Because the product will be an object that matches the Product interface, we can pass it directly and save some lines of code!

Integrating the Server Action

Now that we have created a new server action to create a new Product in the database, we can integrate this server action into our application.

We can find the form used to create a new Product in the file. We can import the function from the file and use it to create a new Product in the database whenever the form is submitted.

Currently the submit handler for the form looks like this:

//
  const handleSubmit = (e: any) => {
    e.preventDefault();
    console.log({ name, category, images, description, price });
  };

Let's import the function and use it to create a new Product in the database whenever the form is submitted.

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

...

const handleSubmit = async (e: any) => {
  e.preventDefault();
  const newProductId = await createProduct({ name, category, description, price, images });
};

Here, we're calling the function with an object containing the , , and fields from the form. This will create a new Product in the database with the provided data.

Insight

If you try to return the new product directly from the function, you may encounter the following warning in the console:

//
Warning: Only plain objects can be passed to Client Components from Server Components.
Objects with toJSON methods are not supported.
Convert it manually to a simple value before passing it to props.

This happens when a server action returns a complex object that cannot be serialized to JSON, to a .

The data returned from mongoose is not a plain object, the We get back is a complex object.

If you ever want to return a full object from a server action to a client component, you need to serialize it first using and to convert it back to a plain object.

products.ts

export async function createProduct(product: Product) {
  await dbConnect();
  const newProduct = await Product.create(product);
  // convert the product to a plain object
  return JSON.stringify(newProduct);
}
//
// on the client side

const result = await createProduct({ name, category, description, price, images });
const newProduct = JSON.parse(result);

There are other ways to clean up object data, but this is a simplest way to do it.

More practically- if we only need specific data, we don't need to return the whole object back to the client. We can return only the data we need.

In our current code, for example, we just need the of the new product, so we'll just return that.

products.ts

export async function createProduct(product: Product) {
  await dbConnect();
  const newProduct = await Product.create(product);
  // convert the product to a plain object
  return newProduct._id.toString();
}

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.

Let's update our function to handle errors that may occur during the creation of a new Product.

products.ts

export async function createProduct(product: Product) {
  try {
    await dbConnect();
    const newProduct = await Product.create(product);
    return newProduct._id.toString();
  } catch (error) {
    console.error("Error creating product:", error);
    throw new Error("Error creating product");
  }
}

Then we can handle the error in the component where we call the function.

//
const handleSubmit = async (e: FormEvent) => {
  e.preventDefault();
  try {
    await createProduct({ name, category, description, price, images });
  } catch (error) {
    // show some toast or alert to the user
    console.error("Error creating product:", error);
  }
};

Redirecting After Creating a Product

After creating a new Product in the database, we may want to redirect the user to a different page. We can use the hook from to redirect the user to a different page after creating a new Product.

AddProduct.tsx
import { useRouter } from "next/navigation";

// ...
export default function AddProduct() {
  const router = useRouter();
  // ...
  const handleSubmit = async (e: FormEvent) => {
    e.preventDefault();
    try {
      const newProductId = await createProduct({ name, category, description, price, images });
      // redirect to the products page after creating a new product
      router.push(`/product/view/${newProductId}`);
    } catch (error) {
      // show some toast or alert to the user
      console.error("Error creating product:", error);
    }
  };
}

Now, when we receive the after creating a new Product, we can use the method to redirect the user to the page, where is the of the new Product.

Conclusion

In this lesson, we learned how to interact with the database using Mongoose using server actions. We created a new server action to create a new Product in the database and integrated this server action into our application. We also learned how to handle errors that may occur during database operations.

We also learned how to redirect the user to a different page after creating a new Product in the database.

In the next lesson, we will learn how to retrieve data from the database using Mongoose using server actions, and how to take the ID of the product we just created and use it to view the product details.

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

Server Actions - Read a Product