CRUD con el stack MEAN
Image by Hitesh Choudhary from Pixabay

En este tutorial te mostraré paso a paso cómo crear un CRUD sencillo con el stack de JavaScript MEAN (Mongo, Express, Angular, Node), te servirá de base para crear otros proyectos y seguir ampliando tus conocimientos. Consta de 3 partes: configurar el frontend, configurar el backend e integración. Al final del tutorial encontrarás la dirección del repositorio de este proyecto en GitHub. Sin más manos a la obra.

Requisitos previos

  1.  Tener instalado Node de manera global en tu sistema operativo.
  2.  Tener instalado Angular CLI.
  3.  Crear una cuenta en Mongodb.com.
  4.  Contar con Postman instalado en tu sistema para testear los endpoints de la API.

Configurar frontend en Angular

1. Crear nuevo proyecto usando Angular CLI

Crea una carpeta llamada crud-stack-mean desde la terminal de comandos ubicate dentro de ella, para crear tu nuevo proyecto en Angular escribe:

ng new productos

Escribes ‘Yes’ en las siguientes 2 preguntas y escoges ‘CSS’ en la pregunta ‘Which stylesheet format would you like to use‘, ahora espera a que se descarguen todas las dependencias que necesita el proyecto.

CRUD con el stack MEAN

Cuando se termine de instalar el proyecto accede desde la terminal de comandos a la carpeta recién creada y enciende el servidor tecleando:

cd productos
ng serve --o

CRUD con el stack MEAN

Se abrirá en el navegador la vista principal de tu proyecto:

Ir al navegador para ver corriendo la app

En tu editor de texto favorito abre la carpeta del proyecto, en mi caso estaré usando VS Code, abre el archivo src/app/app.component.html y borra todo su contenido:

CRUD con el stack MEAN

Agrega una etiqueta H1 con cualquier texto, ya que esto es solo una prueba, yo escribiré Hello World!, ahora, si vas al navegador verás reflejado el cambio:

CRUD con el stack MEAN

2. Instalar dependencias

Para agregar bootstrap a tu proyecto abre una nueva terminal de comandos en la carpeta de tu proyecto y escribe:

npm install bootstrap

Una vez que termine de instalarse con tu editor de texto abre el archivo angular.json que se encuentra en la raíz de tu proyecto y en styles agrega la siguiente línea de código:

"node_modules/bootstrap/dist/css/bootstrap.min.css"

CRUD con el stack MEAN

Para el servidor desde la terminal en donde se encuentra corriendo haciendo la combinación de teclas ‘Ctrl + c’ y vuelve a encender el servidor con el comando:

ng serve --o

En tu navegador se abrirá una nueva pestaña y notarás que han cambiado los estilos en tu proyecto, Bootstrap se ha instalado correctamente.

3. Crear componentes, modelo y servicio

Para este proyecto solo se usarán dos componentes uno para crear y editar y el otro para listar, para ello en la terminal escribe:

ng g c components/create-product

y luego:

ng g c components/list-products

Para crear el servicio, en la terminal escribe:

ng g s services/product

Crea la carpeta src/app/models y dentro de ella el archivo product.ts, en este archivo pondrás las propiedades del modelo y un constructor, para ello agrega el siguiente código:

export class Product{
  _id?:number;
  name: string;
  category: string;
  price: number;
  status: string;

  constructor(name: string, category: string, price: number, status: string){

    this.name = name;
    this.category = category;
    this.price = price;
    this.status = status;
  }
}

CRUD con el stack MEAN

4. Crear routing del proyecto

Con tu editor de texto abre el archivo src/app/app-routing.module.ts en este archivo se configuran las rutas del proyecto, vas a agregar las rutas ‘ ‘ que es la raíz, ‘create-product’, ‘edit-product’ y una ruta más que se encargará de redirigir a la raíz cuando el usuario introduzca una ruta que no exite:

Crear el routing del proyecto CRUD con stack MEAN

import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { CreateProductComponent } from './components/create-product/create-product.component';
import { ListProductsComponent } from './components/list-products/list-products.component';

const routes: Routes = [
  { path: '', component: ListProductsComponent },
  { path: 'create-product', component: CreateProductComponent },
  { path: 'edit-product/:id', component: CreateProductComponent },
  { path: '**', redirectTo: '', pathMatch: 'full' }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule]
})

export class AppRoutingModule { }

Abre el archivo src/app/app.component.html y vuelve a borrar su contenido que pusiste de prueba y agrega la siguiente directiva:

<router-outlet></router-outlet>

En el navegador prueba las rutas http://localhost:4200/create-product, http://localhost:4200/edit-product/1 verás que ambas rutas están funcionando correctamente.

comprobar rutas

 5. Configurando las vistas

Para dar estilos generales a las vistas de tu proyecto abre el archivo src/styles.css y agrega el siguiente código o agrega tu código personalizado:

body {
background: #373B44;
/* fallback for old browsers */
background: -webkit-linear-gradient(to right, #4286f4, #373B44);
/* Chrome 10-25, Safari 5.1-6 */
background: linear-gradient(to right, #4286f4, #373B44);
/* W3C, IE 10+/ Edge, Firefox 16+, Chrome 26+, Opera 12+, Safari 7+ */
}

.title {
font-size: 1.8rem;
font-family: 'Open Sans', sans-serif;
}

Para este proyecto estaré usando para los íconos la librería Font Awesome y para la fuente usaré Open Sans de Google Fonts, abre con tu editor de textos el archivo src/index.html y entre las etiquetas <head></head> agrega las siguientes líneas de código:

<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/5.15.4/css/all.min.css" integrity="sha512-1ycn6IcaQQ40/MKBW2W4Rhis/DbILU74C1vSrLJxCq57o941Ym01SwNsOMqvEBFlcgUa6xLiPY/NS5R+E6ztJQ==" crossorigin="anonymous" referrerpolicy="no-referrer" /> 
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> 
<link href="https://fonts.googleapis.com/css2?family=Open+Sans:wght@800&display=swap" rel="stylesheet">

Abre el archivo src/app/components/list-products/list-products-component.html, borra su contenido y agrega el siguiente código:

<div class="container mt-5">
  <div class="row">
    <div class="col-lg-8 offset-lg-2">
      <div class="card">
        <div class="card-body text-center">
          <span class="title">Product List</span>
          <button class="btn btn-success float-end" routerLink="/create-product">Create Product</button>
          <table *ngIf="listProducts.length > 0" class="table table-striped table-responsive mt-5">
            <thead>
              <tr>
                <th scope="col">Product Name</th>
                <th scope="col">Category</th>
                <th scope="col">Price $USD</th>
                <th scope="col">Status</th>
                <th scope="col"></th>
              </tr>
            </thead>
            <tbody>
              <tr *ngFor="let product of listProducts">
                <td>{{ product.name }}</td>
                <td>{{ product.category }}</td>
                <td>{{ product.price | currency: 'USD'}}</td>
                <td>{{ product.status }}</td>
                <td>
                  <i [routerLink]="['/edit-product', product._id]" class="fas fa-edit text-primary"></i>
                  <i (click)="deleteProduct(product._id)" class="fas fa-trash text-danger"></i>
                </td>
              </tr>
            </tbody>
          </table>
          <h5 class="no-available" *ngIf="listProducts.length == 0">There are no products available</h5>
        </div>
      </div>
    </div>
  </div>
</div>

Recuerda que sólo estás configurando la estructura de los archivos, más adelante agregarás el código necesario para que funcione correctamente tu CRUD con el stack MEAN. Abre el archivo src/app/components/list-products/list-components.css y agrega el siguiente estilo para los íconos:

.fas{
cursor: pointer;
margin-left: 7px;
}

Abre el archivo src/app/components/create-product/create-product.component.html para agregar la estructura del archivo borra el contenido y pega el siguiente código:

<div class="container mt-5">
  <div class="col-lg-6 offset-lg-3">
    <div class="card">
      <div class="card-body text-center">
        <span class="title">{{ title | uppercase }}</span>
        <form class="mt-3" [formGroup]="productForm" (ngSubmit)="addProduct()">
          <div class="mb-3">
            <input type="text" formControlName="name" class="form-control form-control-lg" placeholder="Product name">
            <div class="text-danger"
              *ngIf="productForm.get('name')?.hasError('required') && productForm.get('name')?.touched">
              <span>The name is required</span>
            </div>
          </div>

          <div class="mb-3">
            <input type="text" formControlName="category" class="form-control form-control-lg" placeholder="Category">
            <div class="text-danger"
              *ngIf="productForm.get('category')?.hasError('required') && productForm.get('category')?.touched">
              <span>The category is required</span>
            </div>
          </div>

          <div class="mb-3">
            <input type="number" formControlName="price" class="form-control form-control-lg" placeholder="Price">
            <div class="text-danger"
              *ngIf="productForm.get('price')?.hasError('required') && productForm.get('price')?.touched">
              <span>The price is required</span>
            </div>
          </div>

          <div class="mb-3">
            <input type="text" formControlName="status" class="form-control form-control-lg" placeholder="Status">
            <div class="text-danger"
              *ngIf="productForm.get('status')?.hasError('required') && productForm.get('status')?.touched">
              <span>The status is required</span>
            </div>
          </div>
          <div class="mb-3">
            <button routerLink="/" class="btn btn-secondary btn-lg float-start">Back</button>
            <button type="submit" [disabled]="productForm.invalid"
              class="btn btn-success btn-lg float-end">Save</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>

6. Configurar formulario reactivo

Con tu editor de textos abre el archivo src/app/app.module.ts e importa el modulo ReactiveFormsModule:

CRUD con el stack MEAN

Abre el archivo src/app/components/create-product/create-product.components.ts, vas a configurar las validaciones de tu formulario, para ello agrega las siguientes líneas de código:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Product } from 'src/app/models/product';
import { ProductService } from 'src/app/services/product.service';

@Component({
  selector: 'app-create-product',
  templateUrl: './create-product.component.html',
  styleUrls: ['./create-product.component.css']
})
export class CreateProductComponent implements OnInit {
  productForm: FormGroup;
  title = 'Creat Product';
  id: string | null;

  constructor(private fb: FormBuilder, private router: Router, private _productService: ProductService, private aRouter: ActivatedRoute) {
    this.productForm = this.fb.group({
      name: ['', Validators.required],
      category: ['', Validators.required],
      price: ['', Validators.required],
      status: ['', Validators.required],
    })
    this.id = this.aRouter.snapshot.paramMap.get('id');
  }

  ngOnInit(): void {
    this.Edit();
  }

  addProduct() {

    const PRODUCT: Product = {
      name: this.productForm.get('name')?.value,
      category: this.productForm.get('category')?.value,
      price: this.productForm.get('price')?.value,
      status: this.productForm.get('status')?.value,
    }
    //Verificar existe el producto
    if (this.id !== null) {
      //existe el producto se edita
      this._productService.editProduct(this.id, PRODUCT).subscribe(data => {
        this.router.navigate(['/']);
      }, error => {
        console.log(error);
        this.productForm.reset();
      })
    } else {
      //no existe el producto se crea
      console.log(PRODUCT);
      this._productService.saveProduct(PRODUCT).subscribe(data => {
        this.router.navigate(['/'])
      }, error => {
        console.log(error);
        this.productForm.reset();
      })
    }


  }

  Edit() {

    if (this.id !== null) {
      this.title = 'Edit Product';
      this._productService.getAProduct(this.id).subscribe(data => {
        this.productForm.setValue({
          name: data.name,
          category: data.category,
          price: data.price,
          status: data.status
        })
      })
    }
  }
}

Después de terminar las validaciones toca enlazarlas con tu formulario para ello abre el archivo src/app/components/create-product/create-product.component.html y agrega el siguiente código:

<div class="container mt-5">
  <div class="col-lg-6 offset-lg-3">
    <div class="card">
      <div class="card-body text-center">
        <span class="title">{{ title | uppercase }}</span>
        <form class="mt-3" [formGroup]="productForm" (ngSubmit)="addProduct()">
          <div class="mb-3">
            <input type="text" formControlName="name" class="form-control form-control-lg" placeholder="Product name">
            <div class="text-danger"
              *ngIf="productForm.get('name')?.hasError('required') && productForm.get('name')?.touched">
              <span>The name is required</span>
            </div>
          </div>

          <div class="mb-3">
            <input type="text" formControlName="category" class="form-control form-control-lg" placeholder="Category">
            <div class="text-danger"
              *ngIf="productForm.get('category')?.hasError('required') && productForm.get('category')?.touched">
              <span>The category is required</span>
            </div>
          </div>

          <div class="mb-3">
            <input type="number" formControlName="price" class="form-control form-control-lg" placeholder="Price">
            <div class="text-danger"
              *ngIf="productForm.get('price')?.hasError('required') && productForm.get('price')?.touched">
              <span>The price is required</span>
            </div>
          </div>

          <div class="mb-3">
            <input type="text" formControlName="status" class="form-control form-control-lg" placeholder="Status">
            <div class="text-danger"
              *ngIf="productForm.get('status')?.hasError('required') && productForm.get('status')?.touched">
              <span>The status is required</span>
            </div>
          </div>
          <div class="mb-3">
            <button routerLink="/" class="btn btn-secondary btn-lg float-start">Back</button>
            <button type="submit" [disabled]="productForm.invalid"
              class="btn btn-success btn-lg float-end">Save</button>
          </div>
        </form>
      </div>
    </div>
  </div>
</div>

Para dar algunos estilos al div donde se muestra el mensaje de error abre el archivo src/app/components/create-product/create-product.components.css y agrega las siguientes líneas de código:

.text-danger{ 
   text-align: start; 
   margin-left: 7px 
}

Para ver si hasta aquí esta funcionando correctamente tu CRUD con el stack MEAN ve al navegador y prueba tu app:

Prueba tu app CRUD con el stack MEAN

Las validaciones están funcionando:

CRUD con el stack MEAN

Y si llenas el formulario y das clic en el botón Save te redirige a la página principal y si das clic derecho en el navegador e inspeccionas desde la consola el objeto product trae la información correctamente:

El objeto product se esta generando correctamente

El frontend está listo ahora toca construir el backend.

Configurar backend

1. Instalar paquetes

Desde la terminal de comandos entra a la carpeta de tu proyecto crud-stack-mean y crea una carpeta nueva llamada servidor y accede a ella:

mkdir servidor
cd servidor

Para inicializar el proyecto escribe en la terminal:

npm init

Inmediatamente van a aparecer una serie de preguntas a las cuales debes de responder:

CRUD con el stack MEAN

Ahora toca instalar algunas dependencias que necesitas para el desarrollo del backend, en la terminal escribe una a una las siguientes instrucciones:

npm install -D nodemon
npm install express mongoose dotenv
npm install cors

2. Configurar servidor

Crea el archivo servidor/index.js por el momento pondrás un código de prueba y más adelante lo modificarás, dentro de este nuevo archivo escribe:

console.log('Online!')

Abre el archivo servidor/package.json, en scripts borra la línea:

"test": "echo \"Error: no test specified\" && exit 1"

y agrega:

"dev": "nodemon ."

Debe de verse algo así:

CRUD con el stack MEAN

Con este paquete cuando realizas un cambio en el backend automáticamente vuelve a compilar y se ve reflejado el cambio. Para que corra y se quede a la escucha en la terminal escribe:

npm run dev

Verás que muestra el mensaje que configuraste en el archivo index.js lo que significa que está corriendo sin problemas:

corre el servidor de prueba de tu CRUD con el stack MEAN

Para levantar el servidor abre el archivo servidor/index.js y agrega las siguientes instrucciones:

const express = require('express');

//Se crea el servidor
const app = express();

//Ruta de prueba
app.get('/', (req, res) => {
res.send('Hello World');
})

app.listen(5001, () => {
console.log('The server is runnig')
})

En la terminal donde está corriendo nodemon verás el mensaje que pusiste en el consol.log:

The server is running

Ahora abre el navegador en http://localhost:5001/ verás que el servidor esta corriendo y responde correctamente el mensaje de la ruta de prueba:

CRUD con el stack MEAN

3. Configurar base de datos

Ingresa a tu cuenta en mongodb.com, si no tienes cuenta, crea una. Al ingresar te mostrará el dashboard, crea un proyecto nuevo:

Ingresa a tu cuenta de mongodb.com

Establece un nombre para tu proyecto:

CRUD con el stack MEAN

En la siguiente ventana te preguntan si quieres agregar miembros al proyecto, en mi caso no lo haré:

Puedes agregar miembros al proyecto CRUD MEAN

Da clic en el botón ‘build a database’:

Da click en build a database

Escoge el cloud gratuito y da clic en el botón create:

Escoge el plan FREE

En la siguiente ventana da clic al botón de la parte inferior ‘create cluster’:

Da clic en Create Cluster

Crea un nombre de usuario y password, al terminar da clic en el botón ‘create user’:

CRUD con el stack MEAN

Ahora en el menú de la izquierda ve a ‘Database’:

Ve a Database

Da clic en el botón ‘Connect’:

Realiza una conexion

Escoge si quieres que solo se conecte desde una dirección IP en específico, desde diferentes direcciones IP identificadas o que cualquier dirección IP tenga acceso:

CRUD con el stack MEAN

En mi caso escogí ‘Allow Access from Anywhere’:

Agrega una direccion IP

Escoge como método de conexión ‘Connect using MongoDB Compass’:

Escoge el metodo de conexion

CRUD con el stack MEAN

En la siguiente ventana que se abre, escoge si ya tienes instalado o no MongoDB Compass, sino lo tienes escoge el sistema operativo de tu equipo y da clic en el botón Download Compass para que se descargue el instalador y en el punto 2 copia el string de conexión ya que más adelante lo usarás:

Baja el instalador de MongoDB Compass

3.1 Instala MongoDB Compass

Abre el instalador que descargaste y sigue los pasos de instalación:

Instala MongoDB Compass

Una vez que termine de instalarse MongoDB Compass se abrirá una ventana en donde tienes que pegar la cadena de conexión que copiaste anteriormente agregando el password que configuraste:

Agregar la cadena de conexion

Si todo sale bien en la nueva ventana que se abre ve a la pestaña Databases y da clic en el botón ‘Create database’:

CRUD con el stack MEAN

Asigna un nombre para la base de datos y para la colección:

Crea una nueva base de datos para tu CRUD con el stack MEAN

Se ha creado la nueva base de datos con la que trabajarás con tu CRUD con el stack MEAN.

4. Conectar la base de datos

Crea el archivo servidor/variables.env y en variable DB_MONGO pega la cadena de conexión:

DB_MONGO=mongodb+srv://diario_programador:[email protected]/crudproducts

Recuerda cambiar ‘tupassword’ por tu contraseña y que después del slash va el nombre de tu base de datos. Ahora crea la carpeta servidor/config y dentro crea el archivo db.js al cual agrega el siguiente código:

const mongoose = require('mongoose');
require('dotenv').config({ path: 'variables.env' });

const DBconnection = async () => {

    try {

        await mongoose.connect(process.env.DB_MONGO, {
            useNewUrlParser: true,
            useUnifiedTopology: true,
            useFindAndModify: false
        })
        console.log('BD Conectada');
        
    } catch (error) {
        console.log(error);
        process.exit(1); // Detenemos la app
    }

}

module.exports = DBconnection

En tu archivo servidor/index.js es necesario agregar más instrucciones:

const express = require('express');
const DBconnection =require('./config/db');
const cors = require('cors');

//Se crea el servidor
const app = express();

//Conectar a DB
DBconnection();
app.use(cors())

//habilitar datos JSON
app.use(express.json());

//Ruta
app.use('/api/products', require('./routes/product'));

app.listen(5001, () => {
    console.log('The server is runnig')
})

5. Configurar routes

Crea la carpeta servidor/routes y dentro de ella el archivo product.js y agrega el siguiente código:

//Rutas
const express = require('express');
const router = express.Router();

//api/products
router.post('/', () => {
   console.log('creating a product...')
})

module.exports = router;

Abre el programa Postman para hacer una prueba y usando el verbo POST prueba la ruta http://localhost:5001/api/products una vez que mandes la petición verás en la terminal donde esta corriendo nodemon que hay  una respuesta:

CRUD con el stack MEAN

6. Crear y configurar controlador

Crea la carpeta servidor/controllers y dentro de ella el archivo productControllers.js agrega el siguiente código de prueba:

exports.createProduct = (req, res) => { 
   console.log(req, body); 
}

Ve al archivo servidor/routes/product.js agrega el siguiente código:

//Rutas
const express = require('express');
const router = express.Router();
const productController = require('../controllers/productController');

//api/products
router.post('/', productController.createProduct);

module.exports = router;

7. Modelar la base de datos

Crea la carpeta servidor/models y dentro de ella el archivo Product.js, en este archivo se modelará la base de datos, para ello agrega el siguiente código:

const mongoose = require('mongoose');

const ProductSchema = mongoose.Schema({
    name: {
        type: String,
        required: true
    },
    category: {
        type: String,
        required: true
    },
    price: {
        type: Number,
        required: true
    },
    status: {
        type: String,
        required: true
    },
    createAt: {
        type: Date,
        default: Date.now()
    }
});

module.exports = mongoose.model('Product', ProductSchema);

8. Agregar productos a la base de datos

Abre el archivo del controlador que se encuentra en servidor/controllers/productController.js, en el quita el código de prueba y reemplazalo por el siguiente:

const Product = require("../models/Product")

exports.createProduct = async (req, res) => {

    try {
        let product;
        //Create product
        product = new Product(req.body);

        await product.save();
        res.send(product);

    } catch (error) {
        console.log(error);
        res.status(500).send('There was an error on the server');
    }
}

Para probar abre Postman y agrega un producto mandando la petición con los datos como se muestra en la siguiente imagen:

Vuelve a probar con Postman

Como respuesta verás el objeto con sus propiedades:

Como respuesta obtienes el objeto con sus propiedades

Y si vas a MongoDB Compass verás que se ha guardado la información correctamente:

La informacion se guardo en MongoDB Compass

9. Obtener todos los productos

En el archivo servidor/controllers/productController.js al final agrega el método getProducts el cual traerá todos los productos de la base de datos:

exports.getProducts = async (req, res) => {

    try {

        const product = await Product.find();
        res.json(product);

    } catch (error) {
        console.log(error);
        res.status(500).send('There was an error on the server');
    }
}

Abre el archivo servidor/routes/product.js y agrega la ruta:

router.get('/', productController.getProducts);

10. Actualizar producto

En el archivo servidor/routes/product.js agrega la ruta para actualizar el producto en la base de datos:

router.put('/:id', productController.updateProduct);

Para que tu CRUD con el stack MEAN funcione correctamente en el archivo servidor/controllers/productController.js agrega el método updateProduct al final del mismo:

exports.updateProduct = async (req, res) => {

    try {

        const { name, category, price, status } = req.body;
        let product = await Product.findById(req.params.id);

        if(!product){
            res.status(404).json({msg:'Product not found, try with other product'})
        }

        product.name = name;
        product.category = category;
        product.price = price;
        product.status = status;

        product = await Product.findOneAndUpdate({_id : req.params.id}, product, {new:true})
        res.json(product);

    } catch (error) {
        console.log(error);
        res.status(500).send('There was an error on the server');
    }
}

11. Mostrar un producto

Para mostrar un solo producto por su id, abre el archivo servidor/routes/product.js y agrega la siguiente ruta:

router.get('/:id', productController.getProduct);

Y agrega el método getProduct al archivo servidor/controllers/productController.js:

exports.getProduct = async (req, res) => {

    try {

        let product = await Product.findById(req.params.id);

        if(!product){
            res.status(404).json({msg:'Product not found, try with other product'})
        }

        res.json(product);
        
    } catch (error) {
        console.log(error);
        res.status(500).send('There was an error on the server');
    }
}

12. Eliminar un producto

Para borrar un producto de la base de datos agrega la siguiente ruta al archivo servidor/routes/product.js:

router.delete('/:id', productController.deleteProduct);

Y agrega el método deleteProduct al controlador en servidor/controllers/productController.js:

exports.deleteProduct = async (req, res) => {

    try {

        let product = await Product.findById(req.params.id);

        if(!product){
            res.status(404).json({msg:'Product not found, try with other product'})
        }

        await Product.findOneAndRemove({_id: req.params.id})
        res.json({msg:"Product deleted successfully"})
        
    } catch (error) {
        console.log(error);
        res.status(500).send('There was an error on the server');
    }
}

Integrar el backend y frontend

1. Mostrar listado de productos

Ve a la carpeta productos en donde se muestra el frontend y abre el archivo src/app/app.module.ts, importa el módulo HttpClientModule:

import { HttpClientModule } from '@angular/common/http';

Recuerda agregarlo en imports:

Importa el modulo HttpClientModule

En el archivo src/app/services/product.services.ts agrega el siguiente código:

import { HttpClient } from '@angular/common/http';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs';
import { Product } from '../models/product';

@Injectable({
providedIn: 'root'
})
export class ProductService {

url = 'http://localhost:5001/api/products/';

constructor(private http:HttpClient) { }

getProducts(): Observable<any> {
return this.http.get(this.url);
}
}

Y en el archivo src/app/components/list-prodcts/list-products.component.ts agrega el siguiente código:

import { Component, OnInit } from '@angular/core';
import { Product } from 'src/app/models/product';
import { ProductService } from 'src/app/services/product.service';

@Component({
  selector: 'app-list-products',
  templateUrl: './list-products.component.html',
  styleUrls: ['./list-products.component.css']
})
export class ListProductsComponent implements OnInit {
  listProducts: Product[] = [];

  constructor(private _productService: ProductService) { }

  ngOnInit(): void {
    this.getProducts();
  }

  getProducts() {
    this._productService.getProducts().subscribe(data => {
      console.log(data);
      this.listProducts = data;
    }, error => {
      console.log(error);
    })
  }

  deleteProduct(id: any){
    this._productService.deleteProduct(id).subscribe(data => {
      this.getProducts();
    }, error => {
      console.log(error)
    })
  }

}

Para que se muestre el listado de productos es necesario iterarlos en el archivo src/app/components/list-products/list-products.component.html para ello agrega el siguiente código:

<div class="container mt-5">
  <div class="row">
    <div class="col-lg-8 offset-lg-2">
      <div class="card">
        <div class="card-body text-center">
          <span class="title">Product List</span>
          <button class="btn btn-success float-end" routerLink="/create-product">Create Product</button>
          <table *ngIf="listProducts.length > 0" class="table table-striped table-responsive mt-5">
            <thead>
              <tr>
                <th scope="col">Product Name</th>
                <th scope="col">Category</th>
                <th scope="col">Price $USD</th>
                <th scope="col">Status</th>
                <th scope="col"></th>
              </tr>
            </thead>
            <tbody>
              <tr *ngFor="let product of listProducts">
                <td>{{ product.name }}</td>
                <td>{{ product.category }}</td>
                <td>{{ product.price | currency: 'USD'}}</td>
                <td>{{ product.status }}</td>
                <td>
                  <i [routerLink]="['/edit-product', product._id]" class="fas fa-edit text-primary"></i>
                  <i (click)="deleteProduct(product._id)" class="fas fa-trash text-danger"></i>
                </td>
              </tr>
            </tbody>
          </table>
          <h5 class="no-available" *ngIf="listProducts.length == 0">There are no products available</h5>
        </div>
      </div>
    </div>
  </div>
</div>

Recuerda para que se muestre correctamente la lista de productos tienes que tener dos terminales abiertas en cada carpeta del proyecto, en la carpeta servidor debe de estar corriendo:

npm run dev

y en la carpeta productos:

ng serve --o

En el navegador ve a la dirección http://localhost:4200/ si no hubo ningún error verás el listado de productos traídos desde la base de datos:

CRUD con el stack MEAN

2. Eliminar un producto

Para eliminar un producto en el archivo src/app/services/product.service.ts agrega el método deleteProduct:

deleteProduct(id:string): Observable<any> { 
   return this.http.delete(this.url + id); 
}

También hay que agregar el método deleteProduct en el archivo src/app/components/list-products/list-products.components.ts:

deleteProduct(id: any){
   this._productService.deleteProduct(id).subscribe(data => {
      this.getProducts();
   }, error => {
   console.log(error)
   })
}

Y agregar un evento click en el archivo src/app/components/list-products.component.html dentro de la etiqueta del icono de basura, quedaría así:

<i (click)="deleteProduct(product._id)" class="fas fa-trash text-danger"></i>

3. Crear producto

Abre el archivo src/app/services/product.service.ts y agrega el método saveProduct que será el encargado de conectarse al backend y se guarde el producto en la base de datos:

saveProduct(product:Product): Observable<any> { 
   return this.http.post(this.url, product); 
}

Ve a src/app/components/create-product.components.ts importa el ProductService:

import { ProductService } from 'src/app/services/product.service';

En el constructor agrega:

private _productService: ProductService

 

En el método addProduct() agrega el siguiente código después del console.log(PRODUCT):

this._productService.saveProduct(PRODUCT).subscribe(data => {
   this.router.navigate(['/'])
}, error => {
   console.log(error);
   this.productForm.reset();
})

Al final el código debe de quedar así:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Product } from 'src/app/models/product';
import { ProductService } from 'src/app/services/product.service';

@Component({
  selector: 'app-create-product',
  templateUrl: './create-product.component.html',
  styleUrls: ['./create-product.component.css']
})
export class CreateProductComponent implements OnInit {
  productForm: FormGroup;
  title = 'Creat Product';
  id: string | null;

  constructor(private fb: FormBuilder, private router: Router, private _productService: ProductService, private aRouter: ActivatedRoute) {
    this.productForm = this.fb.group({
      name: ['', Validators.required],
      category: ['', Validators.required],
      price: ['', Validators.required],
      status: ['', Validators.required],
    })
    this.id = this.aRouter.snapshot.paramMap.get('id');
  }

  ngOnInit(): void {
    this.Edit();
  }

  addProduct() {

    const PRODUCT: Product = {
      name: this.productForm.get('name')?.value,
      category: this.productForm.get('category')?.value,
      price: this.productForm.get('price')?.value,
      status: this.productForm.get('status')?.value,
    }
    //Verificar existe el producto
    if (this.id !== null) {
      //existe el producto se edita
      this._productService.editProduct(this.id, PRODUCT).subscribe(data => {
        this.router.navigate(['/']);
      }, error => {
        console.log(error);
        this.productForm.reset();
      })
    } else {
      //no existe el producto se crea
      console.log(PRODUCT);
      this._productService.saveProduct(PRODUCT).subscribe(data => {
        this.router.navigate(['/'])
      }, error => {
        console.log(error);
        this.productForm.reset();
      })
    }


  }

  Edit() {

    if (this.id !== null) {
      this.title = 'Edit Product';
      this._productService.getAProduct(this.id).subscribe(data => {
        this.productForm.setValue({
          name: data.name,
          category: data.category,
          price: data.price,
          status: data.status
        })
      })
    }
  }
}

Desde el navegador prueba creando un producto, verás que está funcionando sin problema.

4. Editar producto

Para que tu CRUD con el stack MEAN logre editar un producto en el archivo src/app/services/product.service.ts agrega los métodos getAProduct y editProduct:

getAProduct(id:string): Observable<any> {
   return this.http.get(this.url + id);
}

editProduct(id:string, product:Product): Observable<any> {
   return this.http.put(this.url + id, product);
}

Agrega la directiva RouterLink en el archivo src/app/components/create-product/create-product.component.html en la etiqueta del icono de editar la cual redirigirá a la ruta edit-product:

<i [routerLink]="['/edit-product', product._id]" class="fas fa-edit text-primary"></i>

Localiza la etiqueta span con la clase ‘title’ en donde se muestra el título:

<span class="title">Product List</span>

Para que se muestre el título dinámico dependiendo si se está creando un producto o editando cambia la etiqueta de arriba por la siguiente:

<span class="title">{{ title | uppercase }}</span>

Abre el archivo src/app/components/create-product/create-product.component.ts, aquí te muestro el código final de este archivo:

import { Component, OnInit } from '@angular/core';
import { FormBuilder, FormGroup, Validators } from '@angular/forms';
import { ActivatedRoute, Router } from '@angular/router';
import { Product } from 'src/app/models/product';
import { ProductService } from 'src/app/services/product.service';

@Component({
  selector: 'app-create-product',
  templateUrl: './create-product.component.html',
  styleUrls: ['./create-product.component.css']
})
export class CreateProductComponent implements OnInit {
  productForm: FormGroup;
  title = 'Creat Product';
  id: string | null;

  constructor(private fb: FormBuilder, private router: Router, private _productService: ProductService, private aRouter: ActivatedRoute) {
    this.productForm = this.fb.group({
      name: ['', Validators.required],
      category: ['', Validators.required],
      price: ['', Validators.required],
      status: ['', Validators.required],
    })
    this.id = this.aRouter.snapshot.paramMap.get('id');
  }

  ngOnInit(): void {
    this.Edit();
  }

  addProduct() {

    const PRODUCT: Product = {
      name: this.productForm.get('name')?.value,
      category: this.productForm.get('category')?.value,
      price: this.productForm.get('price')?.value,
      status: this.productForm.get('status')?.value,
    }
    //Verificar existe el producto
    if (this.id !== null) {
      //existe el producto se edita
      this._productService.editProduct(this.id, PRODUCT).subscribe(data => {
        this.router.navigate(['/']);
      }, error => {
        console.log(error);
        this.productForm.reset();
      })
    } else {
      //no existe el producto se crea
      console.log(PRODUCT);
      this._productService.saveProduct(PRODUCT).subscribe(data => {
        this.router.navigate(['/'])
      }, error => {
        console.log(error);
        this.productForm.reset();
      })
    }


  }

  Edit() {

    if (this.id !== null) {
      this.title = 'Edit Product';
      this._productService.getAProduct(this.id).subscribe(data => {
        this.productForm.setValue({
          name: data.name,
          category: data.category,
          price: data.price,
          status: data.status
        })
      })
    }
  }
}

Si vas al navegador y pruebas tu aplicación verás que edita correctamente los productos. El CRUD está funcionando correctamente.

Conclusión

En este tutorial creaste un CRUD básico con el stack MEAN desde cero y paso paso. Si te ha servido este tutorial te invito a compartirlo en tus redes sociales para alcanzar a más personas y si tienes dudas o comentarios déjalos en la caja de comentarios, estaré al pendiente de ellos. Te mando un saludo.

Repositorio del proyecto

Te puede interesar: CRUD con Laravel 9 y React

3 COMENTARIOS

  1. Error cuando hago un post con postman le meto todos los datos name, category, price, etc en body y raw al pasar 10 segundo da el error ‘Operation `products.insertOne()` buffering timed out after 10000ms’

Deja un comentario

Este sitio usa Akismet para reducir el spam. Aprende cómo se procesan los datos de tus comentarios.