Today, we’re going to create our first NextJS app that uses MongoDB to store information. This will be a simple products app that shows a list of products on the homepage. We will also build the tools to let us add, edit or delete products as needed. Let’s start?
First, we run this command:
npx create-next-app product-list
cd product-list
Now, we’re going to add some extra packages:
npm i bootstrap
npm i mongodb
We make sure that we have running the mongo service. After that, we should create this file .env.local into the root project and add these variables MONGODB_URI and MONGODB_DB_NAME into the file, where the first variable should have the connection string to the mongo service and the second should have the database name.
We’re going to create a new folder named lib
and add the javascript mongo.js
into it. This file should have this code:
import { MongoClient } from 'mongodb';
// URI to connect to the mongo service.
const uri = process.env.MONGODB_URI;
// Database name.
const db_name = process.env.MONGODB_DB_NAME;
const options = {};
// Check the URI exists.
if (!process.env.MONGODB_URI) {
throw new Error('Please add your Mongo URI to .env.local');
}
// Instance the Mongo client.
const client = new MongoClient(uri, options);
// Connect to the mongo client.
const connected = await client.connect();
// Connect to the database.
const connectToDatabase = connected.db(db_name);
export default connectToDatabase;
Then, we’ll start the development server. It has hot reloading built-in and links to the docs on the generated home page.
npx next dev
After that, we’re going to create 2 components inside src/components/
with the next code:
productform.js
import React from 'react';
/**
* This is a product form component.
*/
class ProductForm extends React.Component {
constructor(props) {
super(props);
this.state = {
_id: '',
product_name: '',
quantity: '',
price: ''
};
}
// Update the state after adding or editing a product.
onChangedData = (e, field_name) => {
this.setState({
[field_name]: e.target.value
});
};
// Clear values.
clearValues = () => {
this.setState({
_id: '',
product_name: '',
quantity: '',
price: ''
});
}
// Add a new product.
onAddFormAction = e => {
e.preventDefault();
if (typeof this.props.onAddFormAction == 'function' && this.isValid()) {
this.props.onAddFormAction({
product_name: this.state.product_name,
quantity: this.state.quantity,
price: this.state.price
});
this.clearValues();
}
};
// Edit a product.
onEditFormAction = e => {
e.preventDefault();
if (typeof this.props.onEditFormAction == 'function' && this.isValid()) {
this.props.onEditFormAction(this.state);
this.clearValues();
}
};
// Load the values after selecting a product from the list.
loadValues = product => {
this.setState({
_id: product._id,
product_name: product.product_name,
quantity: product.quantity,
price: product.price
});
};
// Display buttons according to the operation.
showButtons = () => {
if (this.state._id != '') {
return (
<>
<button type="submit" className="btn btn-primary" onClick={this.onEditFormAction}>Edit</button>
<a className="btn btn-danger" onClick={this.clearValues}>Cancel</a>
);
}
return (
<button type="submit" className="btn btn-primary" onClick={this.onAddFormAction}>Add</button>
);
};
// Check if the state is valid.
isValid = () => {
const isValid = Object.keys(this.state).every(key => {
if (key != '_id') {
return this.state[key] != '';
}
return true;
});
if (!isValid) {
alert("There are some fields empty!");
}
return isValid;
};
render = () => {
return (
<div className='card mb-2 mt-2'>
<div className='card-body'>
<h5 className="card-title">Add product</h5>
<hr className='divider' />
<form method='post' name='form-product'>
<div className='row mb-2'>
<div className="col">
<input type="text"
className="form-control"
id="product_name"
name="product_name"
placeholder='Product name'
value={this.state.product_name}
required={true}
onChange={e => this.onChangedData(e, 'product_name')} />
</div>
<div className="col">
<input type="number"
className="form-control"
id="product_quantity"
name="product_quantity"
placeholder='Quantity'
value={this.state.quantity}
required={true}
onChange={e => this.onChangedData(e, 'quantity')} />
</div>
<div className="col">
<input type="number"
className="form-control"
id="product_price"
name="product_price"
placeholder='Price'
value={this.state.price}
required={true}
onChange={e => this.onChangedData(e, 'price')} />
</div>
<div className='col'>
<div className="d-grid gap-2 d-md-flex">
{this.showButtons()}
</div>
</div>
</div>
</form>
</div>
</div>
);
}
}
export default ProductForm;
productlist.js
import React from 'react';
/**
* This is the product list component.
*/
class ProductList extends React.Component {
// Action to edit a product.
onEdit = (e, product) => {
e.preventDefault();
if (typeof this.props.onEdit == 'function') {
this.props.onEdit(product);
}
};
// Action to delete a product.
onDelete = (e, product) => {
e.preventDefault();
if (typeof this.props.onEdit == 'function') {
this.props.onDelete(product);
}
};
render = () => {
return (
<>
<ul className="nav nav-tabs" role="tablist">
<li className="nav-item" role="presentation">
<button className="nav-link active" id="product-list-tab" databstoggle="tab" databstarget="#product-list-tab" type="button" role="tab" aria-controls="product-list-tab" aria-selected="true">Product list</button>
</li>
</ul>
<div className="tab-content">
<div className="tab-pane fade show active" id="home" role="tabpanel" aria-labelledby="product-list-tab">
<table className="table">
<thead className="thead-light">
<tr>
<th scope="col">#</th>
<th scope="col">Product name</th>
<th scope="col">Quantity</th>
<th scope="col">Price</th>
<th scope="col">Action</th>
</tr>
</thead>
<tbody>
{/* List products */}
{
this.props.products.map((p, i) => {
return (
<tr key={p._id}>
<th scope="row">{i + 1}</th>
<td>{p.product_name}</td>
<td>{p.quantity}</td>
<td>{p.price}</td>
<td>
<div className="d-grid gap-2 d-md-flex">
<button className='btn btn-light' onClick={e => this.onEdit(e, p)}>Edit</button>
<button className='btn btn-danger' onClick={e => this.onDelete(e, p)}>Delete</button>
</div>
</td>
</tr>
);
})
}
</tbody>
</table>
</div>
</div>
);
};
}
export default ProductList;
We should add this code to the index.js
import React from 'react';
import ProductForm from '../components/productform';
import ProductList from '../components/productlist';
/**
* Home component to display the main view.
*/
class Home extends React.Component {
constructor(props) {
super(props);
// Create a product form reference.
this.productForm = React.createRef();
this.state = {
products: []
};
}
// After mounting the component, the list is loaded.
componentDidMount = () => {
this.loadList();
};
// Load a list of products.
loadList = () => {
fetch('/api/list').then(response => {
return response.text();
}).then(value => {
this.setState({ products: JSON.parse(value) });
});
}
// Add a new product.
onAddFormAction = data => {
const request = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
};
fetch('/api/addproduct', request)
.then(response => {
if (response.status == 200) {
this.loadList();
}
});
};
// Edit a selected product.
onEditFormAction = data => {
const request = {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(data)
};
fetch('/api/editproduct', request)
.then(response => {
if (response.status == 200) {
this.loadList();
}
});
};
// Load a selected product into the form.
onEdit = product => {
if (typeof this.productForm.current.loadValues == 'function') {
this.productForm.current.loadValues(product);
}
};
// Remove a selected product.
onDelete = product => {
const request = {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(product)
};
fetch('/api/deleteproduct', request)
.then(response => {
if (response.status == 200) {
this.loadList();
}
});
};
render = () => {
return (
<div className='container'>
<div className='row'>
<div className='col'>
<div className="card mt-2">
<div className="card-header">
Product details
</div>
<div className="card-body">
{/* This is the form to add or edit a product. */}
<ProductForm
ref={this.productForm}
onAddFormAction={this.onAddFormAction}
onEditFormAction={this.onEditFormAction} />
{/*This is a component to list products and also has the actions to edit or delete them. */}
<ProductList
products={this.state.products}
onEdit={this.onEdit}
onDelete={this.onDelete} />
</div>
</div>
</div>
</div>
</div>
);}
}
export default Home;
Now, we’re going to add some APIs into src/api/
:
list.js
import connectToDatabase from '../../../lib/mongodb';
import { Db } from 'mongodb';
/**
* API to list a product.
*/
export default async (req, res) => {
// Get the connection to the database.
const db = await connectToDatabase;
// Array of products.
let products = [];
// Check if db is a database object.
if (db instanceof Db) {
// Get a list of products from the collection. By default 20 products
products = await db
.collection("products")
.find({})
.sort({ id: -1 })
.limit(20)
.toArray();
}
// Return 200 and a list of products.
res.status(200).json(products);
};
addproduct.js
import connectToDatabase from '../../../lib/mongodb';
import { Db } from 'mongodb';
/**
* API to add a product.
*/
export default async (req, res) => {
try {
// Get the connection to the database.
const db = await connectToDatabase;
// Check if db is a database object.
if (db instanceof Db) {
// Add a product to the collection.
db.collection('products').insertOne({
'product_name': req.body.product_name,
'quantity': req.body.quantity,
'price': req.body.price
});
}
// Return 200 if everything was successful.
res.status(200).json("Successful!");
} catch (e) {
// Return 500 if there is an error.
res.status(500).json("Error!");
console.error(e);
}
};
editproduct.js
import connectToDatabase from '../../../lib/mongodb';
import { Db, ObjectId } from 'mongodb';
/**
* API to edit a product.
*/
export default async (req, res) => {
try {
// Get the connection to the database.
const db = await connectToDatabase;
// Check if db is a database object.
if (db instanceof Db) {
// Update the product in the collection using the _id.
db.collection('products').updateOne({
_id: ObjectId(req.body._id)
}, {
$set: {
'product_name': req.body.product_name,
'quantity': req.body.quantity,
'price': req.body.price
}
});
}
// Return 200 if everything was successful.
res.status(200).json("Successful!");
} catch (e) {
// Return 500 if there is an error.
res.status(500).json("Error!");
console.error(e);
}
};
deleteproduct.js
import connectToDatabase from '../../../lib/mongodb';
import { Db, ObjectId } from 'mongodb';
/**
* API to delete a product.
*/
export default async (req, res) => {
try {
// Get the connection to the database.
const db = await connectToDatabase;
// Check if db is a database object.
if (db instanceof Db) {
// Delete a product in the collection by _id.
db.collection('products').deleteOne({ "_id": ObjectId(req.body._id) });
}
// Return 200 if everything was successful.
res.status(200).json("Successful!");
} catch (e) {
// Return 500 if there is an error.
res.status(500).json("Error!");
console.error(e);
}
};
And our application should look like this:
Additionally, we can add some validations to our application and make sure the user can’t add an empty product.
If you want to check it out, here is the complete code: github.com/jjosequevedo/product-list