Hero

Mi primera aplicación con NextJs y MongoDB

Mayo 23, 2022

jperez
Next.js
MongoDB

Hoy vamos a crear nuestra primera aplicación NextJS que utiliza MongoDB para almacenar información. Esta será una simple aplicación de productos que muestra una lista de productos en la página principal. También construiremos las herramientas que nos permitan añadir, editar o eliminar productos según sea necesario. ¿Empezamos?

Primero, ejecutamos este comando:

npx create-next-app product-list

cd product-list

Ahora, vamos a añadir algunos paquetes adicionales:

npm i bootstrap

npm i mongodb

Nos aseguramos de que tenemos ejecutado el servicio mongo. Después de eso, debemos crear este archivo .env.local en el proyecto raíz y añadir estas variables MONGODB_URI y MONGODB_DB_NAME en el archivo, donde la primera variable debe tener la cadena de conexión al servicio mongo y la segunda debe tener el nombre de la base de datos.

Vamos a crear una nueva carpeta llamada lib y añadir el javascript mongo.js en ella. Este archivo debe tener este código:

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;

A continuación, iniciaremos el servidor de desarrollo. Tiene incorporada la recarga en caliente y enlaces a los documentos en la página de inicio generada.

npx next dev

Después, vamos a crear 2 componentes dentro de src/components/ con el siguiente código: 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;

Deberíamos añadir este código al 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;

Ahora, vamos a añadir algunas APIs en 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);
    }
};

Y nuestra aplicación debería tener este aspecto:

nextjs products app

Además, podemos añadir algunas validaciones a nuestra aplicación y asegurarnos de que el usuario no pueda añadir un producto vacío.

Si quieres comprobarlo, aquí tienes el código completo: https://github.com/jjosequevedo/product-list

Recibe consejos y oportunidades de trabajo 100% remotas y en dólares de weKnow Inc.