My first app using NextJs and MongoDB

Table of contents

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