My first app using NextJs and MongoDB

Human heart

Human Made

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