
Join the Conversation!
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.
In this lesson, we will learn how to interact with the database using Mongoose using server actions.
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.
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:
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?
//
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!
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.
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();
}
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);
}
};
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.
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.
"Please login to view comments"
Subscribing gives you access to the comments so you can share your ideas, ask questions, and connect with others.