Back to Blog
Technical articles

My first app using NextJs and MongoDB

Updated November 20, 2025

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

Tags:NextJS