MiniCode Guide: Simple Api

Bienvenidos MiniCoders, por fin llega el momento de empezar a construir una aplicación desde cero, como no queremos usar una API pública o datos mock vamos a crear una pequeña API con Node + Express y Mongo como Data Base.

Va ser algo muy sencillo porque estamos en el taller de React y no queremos perder el foco, más adelante esperamos hacer un taller de Node desde 0 aplicando estructuras más complejas y escalables.

Si antes de continuar quieres consultar algo sobre React que hayamos visto en talleres anteriores, aquí te dejamos los otros artículos:

Enlaces de interés


¿Qué necesitamos?

La verdad es que no necesitamos gran cosa, si ya estabas siguiendo el curso de React desde 0 no deberías tener problemas.

  1. Instalar Node: https://nodejs.org/es/
  2. Instalar npm: https://www.npmjs.com/
  3. Crear una cuenta en: https://cloud.mongodb.com/
  4. Crear una cuenta en: https://render.com/

Las cuentas son para tener una Data Base de Mongo y para desplegar de nuestro Server.


¿Cómo creamos un proyecto?

mkdir instaCode
cd instaCode
npm init -y

Estructura de carpetas y ficheros a usar

|_📁 instaCode
    |_📁 src
        |_📁 api -> Carpeta que define modelos-controladores-rutas
            |_📁 users
                    📝 user.controller.js
                    📝 user.model.js
                    📝 user.routes.js
            |_📁 codes
                    📝 code.controller.js
                    📝 code.model.js
                    📝 code.routes.js
            |_📁 tags
                    📝 tag.controller.js
                    📝 tag.model.js
                    📝 tag.routes.js
        |_📁 helpers -> Carpeta funciones auxiliares para dejar el index.js limpio
                📝 db.js
                📝 swagger.js
                📝 utils.js
        |_📁 middlewares -> Carpeta con los middlewares o interceptores
                📝 authorize.js
        📝 index.js -> Fichero disparador o inicializador
    📝 .env
    📝 .gitignore
    📝 LICENSE
    📝 package.json
    📝 README.md
    📝 swagger.yaml

Instalación de dependencias

npm i express mongoose bcrypt cors dotenv jsonwebtoken swagger-ui-express yamljs
npm i -D nodemon

Definir nuestras variables de entorno

En nuestro fichero .env tendremos definidas las variables de entorno de nuestro proyecto, después tendremos que usarlas para la puesta en producción.

MONGO_URI=mongodb+srv://name:password@cluster0.xchvl.mongodb.net/projectName?retryWrites=true&w=majority
PORT=8080
JWT_SECRET=GenerateYourSecretJwt

La URI de mongo la puedes sacar de vuestra DataBase:

Database connect

Conexión DB

Nuestro server necesitará conectarse a nuestra base de datos y para ello hacemos uso de la librería mongoose que nos permite conectarnos de manera muy sencilla. En nuestro fichero db.js generamos nuestra función.

const mongoose = require('mongoose');
require('dotenv').config();

const urlDb = process.env.MONGO_DB;

const connectDb = async () => {
  try {
    const db = await mongoose.connect(urlDb, {
      useNewUrlParser: true,
      useUnifiedTopology: true
    });
    const { name, host } = db.connection;
    console.log(`Connected with db 💾 name: ${name} in host: ${host}`);
  } catch (error) {
    console.error('Error to connect with db 💾', error);
  }
};

module.exports = {
  connectDb
};

Swagger config

Swagger es una herramienta que nos permite generar documentación de nuestra API para facilitar el uso de terceros. Para ello haremos uso de la librería de swagger-ui que partiendo de un fichero .yaml nos genera nuestra documentación. Por ello en nuestro swagger.js

const express = require('express');
const router = express.Router();
const swaggerUi = require('swagger-ui-express');
const YAML = require('yamljs');
const swaggerDocument = YAML.load('../../swagger.yaml');

router.use('/', swaggerUi.serve, swaggerUi.setup(swaggerDocument));

module.exports = router;

Utils

Como es una API sencilla no vamos a usar validadores como yup o joi, por ello vamos a crear un fichero utils donde tendremos algunas funciones validadoras o que nos faciliten nuestro desarrollo dejando otros puntos de código más limpios. En nuestro utils.js tendremos funciones como la validación de email o password, la generación de tokens o una función de formateo de errores.

En caso de que estas funciones útiles crecieran en número y variedad, podríamos crear una carpeta utils donde separar cada paquete de funciones por funcionalidad, pero como hemos comentado antes, en este caso con este archivo podremos trabajar perfectamente en el proyecto.

const jwt = require('jsonwebtoken');

const validationEmail = (email) => {
  const response =
    /^(([^<>()[\]\\.,;:\s@"]+(\.[^<>()[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
  return response.test(String(email).toLocaleLowerCase());
};

const validationPassword = (password) => {
  const response = /^(?=.*[a-z])(?=.*[A-Z])(?=.*[0-9])(?=.*[!@#$%^&*_=+-]).{8,12}$/;
  return response.test(String(password));
};

const generateToken = (id, email) => {
  return jwt.sign({ id, email }, process.env.JWT_SECRET, { expiresIn: '1d' });
};

const verifyToken = (token) => {
  return jwt.verify(token, process.env.JWT_SECRET);
};

const setError = (code, message) => {
  const error = new Error();
  error.code = code;
  error.message = message;
  return error;
};

module.exports = {
  validationEmail,
  validationPassword,
  generateToken,
  verifyToken,
  setError
};

Definiendo los modelos

Los modelos de nuestras APIS son como un contrato de estructura de datos, definimos qué información llegará y de qué tipo. En nuestro caso tendremos un modelo para usuarios, otro para los snippets y por último el de tags.

El más complejo será el de user puesto que antes de guardar la información de un usuario debemos "hashear" su contraseña, esto es por normativa ya que nunca se guarda la contraseña del user. Por ello un ejemplo sería user.model.js

const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const bcrypt = require('bcrypt');
const { validationPassword } = require('../../helpers/utils');

const schema = new Schema(
  {
    username: { type: String, unique: true, required: true },
    email: { type: String, unique: true, required: true },
    password: { type: String, required: true },
    emoji: { type: String, required: true },
    codes: [{ type: Schema.Types.ObjectId, ref: 'Code', required: true }],
    FavCodes: [{ type: Schema.Types.ObjectId, ref: 'Code', required: true }]
  },
  {
    timestamps: true
  }
);

schema.pre('save', function (next) {
  if (!validationPassword(this.password)) {
    return res.status(401).json({ message: 'Unauthorized' });
  }
  this.password = bcrypt.hashSync(this.password, 10);
  next();
});

module.exports = mongoose.model('users', schema);

En el Modelo de Code también tenemos relación con el User y por supuesto con los tags. code.model.js

title: { type: String, unique: true, required: true },
code: { type: String, unique: true, required: true },
author: { type: Schema.Types.ObjectId, ref: "User", required: true },
description: { type: String, maxlength: 300 },
tags: [{ type: Schema.Types.ObjectId, ref: "Tag", required: true }],
likes: [{ type: Schema.Types.ObjectId, ref: "User", required: true }],

Y finalmente los tags estarán en su modelo tag.model.js

{
   title: { type: String, unique: true, required: true },
   color: { type: String, unique: true, required: true }
}

Definiendo Controladores

Un controlador hace las funciones que su propio nombre indica, es la sala de máquinas en la que operaremos y crearemos la funcionalidad que se invocará cada vez que accedamos a una ruta. Por ejemplo, nuestro controlador de code en el archivo code.controller.js, contiene las funciones necesarias para completar un CRUD básico, que te servirá de ejemplo para empezar a trabajar con Node y otros modelos.

const Code = require('./code.model');
const { setError } = require('../../helpers/utils');

const getAll = async (req, res, next) => {
  try {
    const page = req.query.page ? parseInt(req.query.page) : 1;
    const skip = (page - 1) * 20;
    const codes = await Code.find().populate('author', 'username').skip(skip).limit(20);
    return res.json({
      status: 200,
      message: 'Recovered all codes',
      data: { codes: codes }
    });
  } catch (error) {
    return next(setError(500, 'Failed all codes'));
  }
};

const getById = async (req, res, next) => {
  try {
    const { id } = req.params;
    const code = await Code.findById(id).populate('author', 'username');
    if (!code) return next(setError(404, 'Code not found'));
    return res.json({
      status: 200,
      message: 'Recovered all codes',
      data: { code: code }
    });
  } catch (error) {
    return next(setError(500, 'Failed code'));
  }
};

const create = async (req, res, next) => {
  try {
    const code = new Code(req.body);
    const codeInBd = await code.save();
    return res.json({
      status: 201,
      message: 'Created new code',
      data: { code: codeInBd }
    });
  } catch (error) {
    return next(setError(500, 'Failed created code'));
  }
};

const update = async (req, res, next) => {
  try {
    const { id } = req.params;
    const code = new Code(req.body);
    code._id = id;
    const updatedCode = await Code.findByIdAndUpdate(id, code);
    if (!updatedCode) return next(setError(404, 'Code not found'));
    return res.json({
      status: 201,
      message: 'Updated new code',
      data: { code: updatedCode }
    });
  } catch (error) {
    return next(setError(500, 'Failed updated code'));
  }
};

const deleteCode = async (req, res, next) => {
  try {
    const { id } = req.params;
    const deletedCode = await Code.findByIdAndDelete(id);
    if (!deletedCode) return next(setError(404, 'Code not found'));
    return res.json({
      status: 200,
      message: 'deleted new code',
      data: { code: deletedCode }
    });
  } catch (error) {
    return next(setError(500, 'Failed deleted code'));
  }
};

module.exports = {
  getAll,
  getById,
  create,
  update,
  deleteCode
};

Definiendo rutas

Las rutas son los endpoints que atacaremos desde nuestro frontal, en cada fichero de routes definiremos las rutas asociando la funcionalidad definida en nuestros controladores. Para ello necesitamos el Router de express que nos facilitará nuestro trabajo. Os dejamos un ejemplo de routes con code.routes.js

const CodeRoutes = require('express').Router();
const { authorize } = require('../../middleware/authorize');
const { getAll, getById, create, update, deleteCode } = require('./code.controller');

CodeRoutes.get('/', [authorize], getAll);
CodeRoutes.get('/:id', [authorize], getById);
CodeRoutes.post('/', [authorize], create);
CodeRoutes.patch('/:id', [authorize], update);
CodeRoutes.delete('/:id', [authorize], deleteCode);

module.exports = CodeRoutes;

Middleware Auth

Si os fijáis en nuestras rutas tenemos un middleware de auth que define si el usuario está autorizado o no, eso es en base al login, estos middlewares funcionan como interceptores que lanzan una comprobación o funcionalidad cuando atacamos a dicha ruta. Pro ejemplo nuestro authorize.js

const { verifyToken, setError } = require('../helpers/utils');
const User = require('../api/users/user.model');

const authorize = async (req, res, next) => {
  try {
    const token = req.headers.authorization;
    if (!token) return next(setError(401, 'Unauthorized'));
    const parsedToken = token.replace('Bearer ', '');
    const validToken = verifyToken(parsedToken, process.env.JWT_SECRET);
    const user = await User.findById(validToken.id);
    delete user.password;
    req.user = user;
    next();
  } catch (error) {
    return next(error);
  }
};

module.exports = {
  authorize
};

Finalmente tenemos nuestro index.js que es el punto de entrada al servidor, por decirlo de alguna manera hace de orquestador disparando toda la funcionalidad asociada a nuestro proyecto.

// Library
const express = require('express');
const cors = require('cors');
const cookieParser = require('cookie-parser');
// Routes
const UserRoutes = require('./api/users/user.routes');
const CodeRoutes = require('./api/codes/code.routes');
const TagRoutes = require('./api/tags/tag.routes');
// DB
const { connectDb } = require('./helpers/db');
// Port
const PORT = process.env.PORT || 8000;
// inicilizate express
const app = express();
// Connect DataBase
connectDb();
// Headers & Verbs
app.use((req, res, next) => {
  res.header('Access-Control-Allow-Methods', 'GET,PUT,POST,DELETE,PATCH');
  res.header('Access-Control-Allow-Credentials', true);
  res.header('Access-Control-Allow-Headers', 'Content-Type');
  next();
});
// Cors enable
app.use(cors({ origin: (origin, callback) => callback(null, true), credentials: true }));
// Json Data
app.use(express.json({ limit: '1mb' }));
// urlEncoded
app.use(express.urlencoded({ limit: '1mb', extended: true }));
// Routes
app.use('/api/user', UserRoutes);
app.use('/api/code', CodeRoutes);
app.use('/api/tag', TagRoutes);
// Swagger docs route
app.use('/api/docs', require('./helpers/swagger'));
// Routes not found 404
app.use('*', (req, res, next) => {
  const error = new Error();
  error.status = 404;
  error.message = 'Route not found';
  return next(error);
});
// Error handler
app.use((error, req, res, next) => {
  return res.status(error.status || 500).json(error.message || 'Unexpected error');
});
// Enable Language
app.disable('x-powered-by');
// Open Listener Server
app.listen(PORT, () => {
  console.log('Server is running in http://localhost:' + PORT);
});

Te dejamos el repo de Github https://github.com/MiniCodeLab/api-instacode, y con esto hemos dado comienzo al proyecto final del Taller de React desde Cero 🎉. Nos vemos pronto MiniCoders.

  • logo github
  • logo instagram
  • logo linkedin
  • logo twitch
  • logo twitter
  • logo youtube

Creado con amor por Mini Code Lab ❤️