Curso de NodeJS | 3. Proyecto completo con Express

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.

Vamos a comentar la plantilla que tienes en

https://github.com/monteserin/nodejs-template

Características del proyecto

Esta plantilla es muy completa y esta adaptada a proyectos grandes que usen base datos con schema, sockets, MVC, etc.

A continuación te explico como usar una plantilla hecha con NodeJS para tus proyectos. Esta plantilla tiene las siguientes características:

  • Programación funcional.
  • Enfocada para hacer API Rest.
  • Soporte para web sockets.
  • Soporte para swagger.

Para instalarla:

npx degit monteserin/nodejs-template

Código fuente

Base de datos

En el fichero .env estableceremos los datos de conexión contra la base de datos y si esta se regenerará de nuevo con cada actualización.

./env

# DATABASE
DB_HOST = localhost
DB_USER = [user]
DB_PASSWORD = [password]
DB_NAME = [name]
DB_FORCE_CLEAN = true

# APP
PORT = 5000

API REST Controller

Los endpoints de la entidad user se gestionan en ./src/entities/user/controller/index.js

En ./src/entities/http-samples.js tienes algunos ejemplos de endpoints.

Ejemplo de petición:

router.get('/:id', asyncHandler(async (req, res) => {
    const { query: { id } } = req;
    const data = await Controller.getById(id);
    res.send(data);
}));

asyncHandler

Se encarga de evitar que el servidor se caiga cuando se produzca una excepción (en nuestro caso usamos Heroku, aunque en local usemos nodemon y no se caerá).

Por otra parte, también nos evita tener que repetir el código de manejo de errores (try, catch) para cada petición. Sólo hará falta escribir dicho código dentro del asyncHandler.

El siguiente código:

app.get("/", asyncHandler(async (req, res) => {
    res.seeeend("va bien la cosaa"); 
}));

daría un error que sería gestionado aquí:

./src/middlewares/error-handler.js

export const asyncHandler = controller => (req, res, next) => Promise
  .resolve(controller(req, res, next))
  .catch((err) => {
    console.error('Request error:' + err.toString());
    if (err) return res.sendStatus(500).send();
});

swagger

Podemos acceder a la documentación de la api REST (Swagger) mediante el path /docs.

Model

Cada uno de los modelos heredará de un modelo genérico. De esta forma, cada modelo tendrá los métodos de alta, baja, modificación y consulta del modelo genérico, además de los suyos propios:

/src/entities/user/model/index.js

import GenericModel from '@Application/repository/generic-model';
import Schema from './schema';

const Model = {
    ...GenericModel(Schema),
    getByEmail: email => Schema.findOne({ where: { email } })
};

export default Model;

/src/entities/user/model/generic-model.js


const GenericModel = Model => ({
    create(data) {
        return Model.create(data);
    },
    get(conditions) {
        return Model.findAll(conditions ? {where:conditions} : {});
    },
    getById(id) {
        return Model.findOne({ where: { id } });
    },
    updateById(id, data) {
        return Model.update(data, { where: { id } });
    },
    deleteById(id) {
        return Model.destroy({ where: { id } });
    },
    findOrCreate(condition, newObj){
        return Model.findOrCreate({where:condition, defaults: newObj});
    }
});

export default GenericModel;

Y la estructura de cada uno de estos modelos estará en su correspondiente fichero schema:

/src/entities/user/model/schema.js

import { db, DataTypes } from '@Application/database';

export default db.define('user', {
	email: DataTypes.STRING,
	auth0Id: DataTypes.STRING,
});

Generación del modelo en la base de datos al arrancar la aplicación

Debemos cargar un controlador que a su vez cargue el modelo cuya estructura queremos persistir.

./src/controllers/index.js

export default function (app) {

  app.use('/plays', PlaysController);

./src/controllers/plays/index.js

import * as PlaysModel from "@Models/plays";

Regenerar la base de datos

Muchas veces, como consecuencia de haber modificado nuestro modelo de datos, necesitaremos borrar la base de datos para que las tablas vuelvan a generarse. Esto nunca debe hacerse en producción. Para forzar el borrado de las tablas al arrancar la aplicación, usaremos {force: true}

./src/database/index.js

export default async (onConnect) => {
	try {
        await db.authenticate();
        db.sync({ force: true});
        ...

Conexión a la base de datos:

Está en /src/application/database/index.js. Desde este fichero cargaremos los datos del fichero .env.

Consultas Many to Many

Schema De Classroom

const ClassRoom = db.define('classroom', {
	...
});

ClassRoom.associate = ({ student }) => {
	ClassRoom.belongsToMany(student, { through: 'classroomsusers', onDelete: 'cascade' });
};

Schema de Student

import {db} from '@Application/database';
const {DataTypes} = require('sequelize');

const Student = db.define('student', {
   ...
});

Student.associate = ({ classroom }) => {
    Student.belongsToMany(classroom, { through: 'classroomsusers', onDelete: 'cascade' });
};

export default Student;

Definir la tabla intermedia con atributos de relación

Los atributos de relación que hemos añadido en este caso son hasTerminated y isInClassroom.

En la tabla que usamos para establecer las relaciones tendremos que definir expresamente el campo id.

import {db, DataTypes} from '@Application/database'

const ClassroomUser = db.define('classroomuser', {
    id: {
        type: DataTypes.INTEGER,
        primaryKey: true,
        autoIncrement: true,
        allowNull: false,
    },
    hasTerminated: DataTypes.BOOLEAN,
    isInClassroom: DataTypes.BOOLEAN
});

ClassroomUser.associate = ({classroom, student}) => {
    ClassroomUser.belongsTo(classroom);
    ClassroomUser.belongsTo(student);
};

export default ClassroomUser;

Configuración de las relaciones

export const setAssociations = (db) => {
	Object.keys(db.models).forEach((modelName) => {
		if ('associate' in db.models[modelName]) {
			db.models[modelName].associate(db.models);
		}
	});
};

Operaciones con relaciones Many to Many

Consulta de una clase con sus respectivos alumnos
import Schema from './schema';
import Student from '../../student/model/schema';

const Model = {
    getClassroomWithStudents(conditions){
        return Schema.findOne({where:conditions,include: Student});
    }
};

export default Model;
Crear un estudiante vinculado a una clase
const [classroom] = await ClassroomModel.get({teacherId:msg.teacherId, cod: msg.cod});
if (classroom) {
  const student = await Model.create({auth0Id: msg.auth0Id, picture: msg.picture, name: msg.name});
  // creamos la relación en la tabla de relaciones
  ClassroomStudentModel.create({studentId: student.id, classroomId: classroom.id}); 
  return classroom;
}

Pasos a seguir al crear una nueva entidad:

1. Duplicamos una entidad de la carpeta entities.

2. Añadimos el adapter de la nueva entidad a /src/entities/index.js.

3. Modificamos el Swagger (sobre todo las rutas).

Login

Desde el front, enviaremos el usuario y contraseña utilizando basic auth:

axios.post("http://localhost:8080/login",{},
  {
    headers: {
      "Content-Type": "application/json",
      Authorization: "basic " + btoa(userName + ":" + password),
    },
  }
).then((response) => console.log(response),(error) => console.log(error));

Desde el back, comprobaremos que el token enviado es válido.

Por 9.99€ al mes tendrás acceso completo a todos los cursos. Sin matrícula ni permanencia.