# API REST

Comenzaremos a trabajar en nuestro MERN/MEVN utilizando:

  • Node.js
  • Express
  • MongoDB
  • React/Vue

Separaremos lo que es backend del frontend, por ende este mismo proyecto de back nos servirá tanto para Vue o React.

TIP

😍😍😍 Más clases en vivo gratis aquí: twitch.tv/bluuweb (opens new window) 🤙🤙🤙

¿Quieres apoyar los directos? 😍

Tienes varias jugosas alternativas:

  1. Suscríbete al canal de Youtube (es gratis) click aquí (opens new window)
  2. Si estás viendo un video no olvides regalar un 👍 like y comentario 🙏🏼
  3. También puedes ser miembro del canal de Youtube click aquí (opens new window)
  4. Puedes adquirir cursos premium en Udemy 👇🏼👇🏼👇🏼

# Requisitos

# Primeros pasos

npm init -y
npm i bcryptjs cookie-parser cors dotenv express express-validator jsonwebtoken mongoose
npm i -D nodemon

package.json

{
    "type": "module",
    "scripts": {
        "dev": "nodemon .",
        "start": "node index.js"
    }
}

Crear:

index.js
.env
.gitignore
README.md
└── controllers
└── database
└── helpers
└── middlewares
└── models
└── routes

.gitignore

node_modules
.env

index.js

import "dotenv/config";
import "./database/connectdb.js";
import express from "express";
import cors from "cors";
import cookieParser from "cookie-parser";
import authRoutes from "./routes/auth.route.js";

const app = express();

app.use(cookieParser());
app.use(express.json());
app.use("/api/v1/auth", authRoutes);

const PORT = process.env.PORT || 5000;
app.listen(PORT, console.log("😍😍 http://localhost:" + PORT));

# Mongoose

database/connectdb.js

import mongoose from "mongoose";

try {
    await mongoose.connect(process.env.DB_URI);
    console.log("😎😎 db conectada");
} catch (error) {
    console.log("😒😒" + error);
}

# Schema & Models

  • Schema (opens new window): Con Mongoose, todo se deriva de un esquema.
  • Cada esquema se asigna a una colección MongoDB y define la forma de los documentos dentro de esa colección.
  • Para usar nuestra definición de esquema, necesitamos convertirla a un modelo con el que podamos trabajar.

models/User.js

import mongoose from "mongoose";
import bcrypt from "bcryptjs";

const userSchema = new mongoose.Schema({
    email: {
        type: String,
        required: true,
        trim: true,
        unique: true,
        lowercase: true,
        index: { unique: true },
    },
    password: {
        type: String,
        required: true,
    },
});

userSchema.pre("save", async function(next) {
    const user = this;

    if (!user.isModified("password")) return next();

    try {
        const salt = await bcrypt.genSalt(10);
        user.password = await bcrypt.hash(user.password, salt);
        next();
    } catch (error) {
        console.log(error);
        throw new Error("Error al codificar la contraseña");
    }
});

userSchema.methods.comparePassword = async function(candidatePassword) {
    return await bcrypt.compare(candidatePassword, this.password);
};

export const User = mongoose.model("User", userSchema);

# Routes

index.js

import authRoutes from "./routes/auth.route.js";
...
app.use("/api/v1/auth", authRoutes);

routes/auth.route.js

import express from "express";
import {
    login,
    register,
    infoUser,
    refreshToken,
    logout,
} from "../controllers/auth.controller.js";

const router = express.Router();

router.post("/register", register);
router.post("/login", login);
router.get("/protected", validateToken, infoUser);
router.get("/refresh", refreshToken);
router.get("/logout", logout);

export default router;

# Controllers

controllers/auth.controller.js

export const register = async (req, res) => {};

export const login = async (req, res) => {};

export const infoUser = async (req, res) => {};

export const refreshToken = (req, res) => {};

export const logout = (req, res) => {};

# Register

routes/auth.route.js

import express from "express";
import {
    login,
    register,
    infoUser,
    refreshToken,
    logout,
} from "../controllers/auth.controller.js";
import { validatorExpress } from "../middlewares/validatorExpress.js";
import { body } from "express-validator";

const router = express.Router();

router.post(
    "/register",
    [
        body("email", "Ingrese un email válido")
            .trim()
            .isEmail()
            .normalizeEmail(),
        body("password", "Contraseña mínimo 6 carácteres")
            .trim()
            .isLength({ min: 6 })
            .custom((value, { req }) => {
                if (value !== req.body.repassword) {
                    throw new Error("No coinciden las contraseñas");
                }
                return value;
            }),
    ],
    validatorExpress,
    register
);

middlewares/validatorExpress.js

import { validationResult } from "express-validator";
export const validatorExpress = (req, res, next) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
        return res.status(400).json({ error: errors.array() });
    }
    next();
};

controllers/auth.controller.js

import jwt from "jsonwebtoken";
import { User } from "../models/User.js";

export const register = async (req, res) => {
    try {
        const { email, password } = req.body;

        let user = await User.findOne({ email });
        if (user) throw new Error("Email ya registrado 😒");

        user = new User({ email, password });
        await user.save();

        // Generar token
        // const { token, expiresIn } = generateToken(user.id);
        // generateRefreshToken(user.id, res);
        // return res.json({ token, expiresIn });
    } catch (error) {
        console.log(error);
        return res.status(403).json({ error: error.message });
    }
};

# Login

routes/auth.route.js

router.post(
    "/login",
    [
        body("email", "Ingrese un email válido")
            .trim()
            .isEmail()
            .normalizeEmail(),
        body("password", "Contraseña mínimo 6 carácteres")
            .trim()
            .isLength({ min: 6 }),
    ],
    validatorExpress,
    login
);

controllers/auth.controller.js

export const login = async (req, res) => {
    try {
        const { email, password } = req.body;
        console.log(req.body);

        let user = await User.findOne({ email });
        if (!user || !(await user.comparePassword(password)))
            throw new Error("Email or password is incorrect");

        // const { token, expiresIn } = generateToken(user.id);
        // generateRefreshToken(user.id, res);
        // return res.json({ token, expiresIn });
    } catch (error) {
        console.log(error);
        return res.status(403).json({ error: error.message });
    }
};

# JWT

helpers/generateTokens.js

URI_MONGO=
JWT_SECRET=
JWT_REFRESH=
MODO=developer
import jwt from "jsonwebtoken";

export const generateToken = (uid) => {
    const expiresIn = 1000 * 60 * 15;
    const token = jwt.sign({ uid }, process.env.JWT_SECRET, { expiresIn });
    return { token, expiresIn };
};

export const generateRefreshToken = (uid, res) => {
    const expiresIn = 1000 * 60 * 60 * 24 * 30;
    const refreshToken = jwt.sign({ uid }, process.env.JWT_REFRESH, {
        expiresIn,
    });

    res.cookie("refreshToken", refreshToken, {
        httpOnly: true,
        secure: !(process.env.MODO === "developer"),
        expires: new Date(Date.now() + expiresIn),
    });
};

controllers/auth.controller.js

import jwt from "jsonwebtoken";
import { User } from "../models/User.js";
import {
    generateToken,
    generateRefreshToken,
} from "../helpers/generateTokens.js";

export const register = async (req, res) => {
    try {
        const { email, password } = req.body;

        let user = await User.findOne({ email });
        if (user) throw new Error("Email ya registrado 😒");

        user = new User({ email, password });
        await user.save();

        const { token, expiresIn } = generateToken(user.id);
        generateRefreshToken(user.id, res);

        return res.json({ token, expiresIn });
    } catch (error) {
        console.log(error);
        return res.status(403).json({ error: error.message });
    }
};

export const login = async (req, res) => {
    try {
        const { email, password } = req.body;
        console.log(req.body);

        let user = await User.findOne({ email });
        if (!user || !(await user.comparePassword(password)))
            throw new Error("Email or password is incorrect");

        const { token, expiresIn } = generateToken(user.id);
        generateRefreshToken(user.id, res);

        return res.json({ token, expiresIn });
    } catch (error) {
        console.log(error);
        return res.status(403).json({ error: error.message });
    }
};

# Ruta protegida

routes/auth.route.js

import { validateToken } from "../middlewares/validateToken.js";
...
router.get("/protected", validateToken, infoUser);
router.get("/logout", logout);

middlewares/validateToken.js

import jwt from "jsonwebtoken";
import { errorTokens } from "../utils/errorsToken.js";

export const valitateToken = (req, res, next) => {
    try {
        let token = req.headers?.authorization;
        if (!token) throw new Error("No existe el token");

        token = token.split(" ")[1];
        const { uid } = jwt.verify(token, process.env.JWT_SECRET);
        req.uid = uid;
        next();
    } catch (error) {
        console.log(error);
        return res.status(401).json({ error: errorTokens(error.message) });
    }
};

helpers/errorsToken.js

export const errorTokens = (message) => {
    switch (message) {
        case "jwt malformed":
            return "Formato no válido";
        case "invalid token":
        case "jwt expired":
        case "invalid signature":
            return "Token no válido";
        default:
            return message;
    }
};

controllers/auth.controller.js

export const infoUser = async (req, res) => {
    try {
        const user = await User.findById(req.uid).lean();
        delete user.password;
        return res.json({ user });
    } catch (error) {
        console.log(error);
        return res.status(403).json({ error: error.message });
    }
};

export const logout = (req, res) => {
    // https://stackoverflow.com/questions/27978868/destroy-cookie-nodejs
    res.clearCookie("refreshToken");
    return res.json({ ok: true });
};

# Refresh Token

routes/auth.route.js

router.get("/refresh", refreshToken);

controllers/auth.controller.js

import {
    generateToken,
    generateRefreshToken,
} from "../helpers/generateTokens.js";
import { errorsToken } from "../helpers/errorsToken.js";

export const refreshToken = (req, res) => {
    try {
        let refreshTokenCookie = req.cookies?.refreshToken;
        if (!refreshTokenCookie) throw new Error("No existe el refreshToken");

        const { id } = jwt.verify(refreshTokenCookie, process.env.JWT_REFRESH);

        const { token, expiresIn } = generateToken(id);

        return res.json({ token, expiresIn });
    } catch (error) {
        console.log(error);
        const data = errorsToken(error);
        return res.status(401).json({ error: data });
    }
};

# Simple HTML

app.use(express.static("public"));

public/index.html

<!DOCTYPE html>
<html lang="es">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Login</title>
    </head>
    <body>
        <form id="formLogin">
            <input type="email" value="jhonatan@test.com" id="email" />
            <input type="password" value="123123" id="password" />
            <button type="submit">Acceder</button>
        </form>

        <script>
            const formLogin = document.querySelector("#formLogin");
            const email = document.querySelector("#email");
            const password = document.querySelector("#password");

            formLogin.addEventListener("submit", async (e) => {
                e.preventDefault();
                try {
                    const res = await fetch("/api/v1/auth/login", {
                        method: "post",
                        headers: {
                            "Content-Type": "application/json",
                        },
                        body: JSON.stringify({
                            email: email.value,
                            password: password.value,
                        }),
                    });

                    console.log(res.ok, res.status);
                    const { token } = await res.json();

                    window.location.href = "/protected.html";
                } catch (error) {
                    console.log(error);
                }
            });
        </script>
    </body>
</html>

public/protected.html

<!DOCTYPE html>
<html lang="en">
    <head>
        <meta charset="UTF-8" />
        <meta http-equiv="X-UA-Compatible" content="IE=edge" />
        <meta name="viewport" content="width=device-width, initial-scale=1.0" />
        <title>Ruta protegida</title>
    </head>
    <body>
        <h1>Ruta protegida</h1>
        <div id="app">
            <h2>Email</h2>
            <h3>UID</h3>
        </div>
        <button id="logout">Logout</button>

        <script>
            document.addEventListener("DOMContentLoaded", async (e) => {
                const app = document.querySelector("#app");
                try {
                    const resToken = await fetch("/api/v1/auth/refresh", {
                        credentials: "include",
                    });
                    console.log(resToken.ok, resToken.status);
                    const { token } = await resToken.json();
                    // console.log(token);

                    const res = await fetch("/api/v1/auth/protected", {
                        headers: {
                            Authorization: "Basic " + token,
                        },
                    });
                    console.log(res.ok, res.status);

                    if (!res.ok) {
                        window.location.href = "/";
                    }

                    const { user } = await res.json();
                    console.log(user);

                    app.innerHTML = `
                        <h2>Email: ${user.email}</h2>
                        <h3>UID: ${user._id}</h3>
                    `;
                } catch (error) {
                    console.log(error);
                }

                const logout = document.querySelector("#logout");
                logout.addEventListener("click", async () => {
                    const res = await fetch("/api/v1/auth/logout");
                    console.log(res.ok, res.status);
                    if (res.ok) {
                        window.location.href = "/";
                    }
                });
            });
        </script>
    </body>
</html>

# Mejoras 1.0

utils/tokenManager.js

import jwt from "jsonwebtoken";
export const generateToken = (uid) => {
    const expiresIn = 60 * 15;

    try {
        const token = jwt.sign({ uid }, process.env.JWT_SECRET, { expiresIn });
        return { token, expiresIn };
    } catch (error) {
        console.log(error);
    }
};

export const generateRefreshToken = (uid, res) => {
    const expiresIn = 60 * 60 * 24 * 30;
    try {
        const refreshToken = jwt.sign({ uid }, process.env.JWT_REFRESH, {
            expiresIn,
        });

        res.cookie("refreshToken", refreshToken, {
            httpOnly: true,
            secure: !(process.env.MODO === "developer"),
            expires: new Date(Date.now() + expiresIn * 1000),
        });
    } catch (error) {
        console.log(error);
    }
};

export const tokenVerificationErrors = {
    "invalid signature": "La firma del JWT no es válida",
    "jwt expired": "JWT expirado",
    "invalid token": "Token no válido",
    "No Bearer": "Utiliza formato Bearer",
    "jwt malformed": "JWT formato no válido",
};

auth.route.js

router.get("/refresh", requireRefreshToken, refreshToken);

middlewares/requireRefreshToken.js

import jwt from "jsonwebtoken";
import { tokenVerificationErrors } from "../utils/tokenManager.js";

export const requireRefreshToken = (req, res, next) => {
    try {
        const refreshTokenCookie = req.cookies.refreshToken;
        if (!refreshTokenCookie) throw new Error("No existe el token");

        const { uid } = jwt.verify(refreshTokenCookie, process.env.JWT_REFRESH);

        req.uid = uid;
        next();
    } catch (error) {
        console.log(error);
        return res
            .status(401)
            .send({ error: tokenVerificationErrors[error.message] });
    }
};

controllers/auth.controller.js (refreshToken)

export const refreshToken = (req, res) => {
    try {
        const { token, expiresIn } = generateToken(req.uid);
        return res.json({ token, expiresIn });
    } catch (error) {
        console.log(error);
        return res.status(500).json({ error: "error de server" });
    }
};

# validatorManager.js

auth.route.js

import { Router } from "express";
import {
    infoUser,
    login,
    logout,
    refreshToken,
    register,
} from "../controllers/auth.controller.js";
import { requireRefreshToken } from "../middlewares/requireRefreshToken.js";
import { requireToken } from "../middlewares/requireToken.js";
import {
    loginValidator,
    registerValidator,
    tokenCookieValidator,
    tokenHeaderValidator,
} from "../middlewares/validatorManager.js";

const router = Router();

router.post("/register", registerValidator, register);

router.post("/login", loginValidator, login);

router.get("/protected", tokenHeaderValidator, requireToken, infoUser);
router.get("/refresh", tokenCookieValidator, requireRefreshToken, refreshToken);

router.get("/logout", logout);

export default router;

middlewares/validatorManager.js

import { body, cookie, header, validationResult } from "express-validator";

const validationResultExpress = (req, res, next) => {
    const errors = validationResult(req);

    if (!errors.isEmpty()) {
        return res.status(400).json({ errors: errors.array() });
    }

    next();
};

export const registerValidator = [
    body("email", "Formato de email incorrecto")
        .trim()
        .isEmail()
        .normalizeEmail(),
    body("password", "Mínimo 6 carácteres")
        .trim()
        .isLength({ min: 6 }),
    body("password", "Formato de password incorrecta").custom(
        (value, { req }) => {
            if (value !== req.body.repassword) {
                throw new Error("No coinciden las contraseñas");
            }
            return value;
        }
    ),
    validationResultExpress,
];

export const loginValidator = [
    body("email", "Formato de email incorrecto")
        .trim()
        .isEmail()
        .normalizeEmail(),
    body("password", "Mínimo 6 carácteres")
        .trim()
        .isLength({ min: 6 }),
    validationResultExpress,
];

export const tokenHeaderValidator = [
    header("authorization", "No existe el token")
        .trim()
        .notEmpty()
        .escape(),
    validationResultExpress,
];

export const tokenCookieValidator = [
    cookie("refreshToken", "No existe refresh Token")
        .trim()
        .notEmpty()
        .escape(),
    validationResultExpress,
];

model/Link.js

import mongoose from "mongoose";
const { Schema } = mongoose;

const linkSchema = new Schema({
    longLink: {
        type: String,
        required: true,
        trim: true,
    },
    nanoLink: {
        type: String,
        unique: true,
        required: true,
        trim: true,
    },
    uid: {
        type: Schema.Types.ObjectId,
        ref: "User",
        required: true,
    },
});

export const Link = mongoose.model("Link", linkSchema);

routes/link.route.js

import { Router } from "express";

const router = Router();

// GET      api/v1/links                all links
// GET      api/v1/links/:nanoLink      search link
// POST     api/v1/links                create link
// PATCH    api/v1/links                update link
// DELETE   api/v1/links/:nanoLink      remove link

export default router;

index.js

import cookieParser from "cookie-parser";
import "dotenv/config";
import express from "express";
import "./database/connectdb.js";
import authRouter from "./routes/auth.route.js";
import linkRouter from "./routes/link.route.js";

const app = express();
app.use(express.json());
app.use(cookieParser());
app.use("/api/v1/auth", authRouter);
app.use("/api/v1/links", linkRouter);

// solo para el ejemplo de login/token
app.use(express.static("public"));

const PORT = process.env.PORT || 5000;
app.listen(PORT, () => console.log("🔥🔥🔥 http://localhost:" + PORT));

controllers/link.controller.js

import { Link } from "../models/Link.js";

export const getLinks = async (req, res) => {
    try {
        const links = await Link.find({ uid: req.uid }).lean();
        return res.json({ links });
    } catch (error) {
        console.log(error);
        return res.status(500).json({ error: "Error de servidor" });
    }
};

routes/link.route.js

import { Router } from "express";
import { getLinks } from "../controllers/link.controller.js";
import { requireToken } from "../middlewares/requireToken.js";
import { tokenHeaderValidator } from "../middlewares/validatorManager.js";

const router = Router();

router.get("/", tokenHeaderValidator, requireToken, getLinks);

export default router;

middlewares/validatorManager.js

export const linkValidator = [
    body("longLink", "Formato link incorrecto")
        .trim()
        .custom(async (value) => {
            try {
                if (!value.startsWith("http")) {
                    value = "https://" + value;
                }
                await axios.get(value);
                return value;
            } catch (error) {
                throw new Error("Link 404 not found");
            }
        }),
    validationResultExpress,
];

routes/link.route.js

router.post("/", tokenHeaderValidator, requireToken, linkValidator, createLink);

controllers/link.controller.js

export const createLink = async (req, res) => {
    try {
        const { longLink } = req.body;
        const link = new Link({ longLink, nanoLink: nanoid(6), uid: req.uid });
        const newLink = await link.save();
        res.json({ newLink });
    } catch (error) {
        console.log(error);
        return res.status(500).json({ error: "Error de servidor" });
    }
};

link.route.js

router.delete(
    "/:id",
    tokenHeaderValidator,
    requireToken,
    paramsLinkValidator,
    removeLink
);

middlewares/validatorManager.js

export const paramsLinkValidator = [
    param("id", "Formato id incorrecto")
        .trim()
        .notEmpty()
        .escape(),
    validationResultExpress,
];

link.controller.js

export const removeLink = async (req, res) => {
    try {
        const { id } = req.params;
        const link = await Link.findById(id);

        if (!link) return res.status(404).json({ error: "no existe link" });

        if (!link.uid.equals(req.uid))
            return res.status(401).json({ error: "no es tu link payaso 🤡" });

        await link.remove();
        return res.json({ link });
    } catch (error) {
        console.log(error);
        if (error.kind === "ObjectId")
            return res.status(403).json({ error: "Formato id incorrecto" });
        return res.status(500).json({ error: "Error de servidor" });
    }
};

link.route.js

router.patch(
    "/:id",
    requireToken,
    paramLinkValidator,
    bodyLinkValidator,
    updateLink
);

link.controller.js

export const updateLink = async (req, res) => {
    try {
        const { id } = req.params;
        let { longLink } = req.body;
        if (!longLink.startsWith("https://")) {
            longLink = "https://" + longLink;
        }

        const link = await Link.findById(id);

        if (!link) return res.status(404).json({ error: "No existe el link" });

        if (!link.uid.equals(req.uid))
            return res.status(401).json({ error: "No le pertenece ese id 🤡" });

        link.longLink = longLink;

        await link.save();

        return res.json({ link });
    } catch (error) {
        console.log(error);
        if (error.kind === "ObjectId") {
            return res.status(403).json({ error: "Formato id incorrecto" });
        }
        return res.status(500).json({ error: "error de servidor" });
    }
};
// router.get("/:id", requireToken, getLink);
router.get("/:nanoLink", paramNanoLinkValidator, getNanoLink);
export const paramNanoLinkValidator = [
    param("nanoLink", "Formato no válido (expressValidator)")
        .trim()
        .notEmpty()
        .escape(),
    validationResultExpress,
];
// busqueda por nanoLink
export const getNanoLink = async (req, res) => {
    try {
        const { nanoLink } = req.params;
        const link = await Link.findOne({ nanoLink });

        if (!link) return res.status(404).json({ error: "No existe el link" });

        return res.json({ longLink: link.longLink });
    } catch (error) {
        console.log(error);
        if (error.kind === "ObjectId") {
            return res.status(403).json({ error: "Formato id incorrecto" });
        }
        return res.status(500).json({ error: "error de servidor" });
    }
};

# Redireccionamiento (opcional)

index.js

app.use("/", redirectRouter);

redirect.route.js

import { Router } from "express";
import { redirectNanoLink } from "../controllers/redirect.controller.js";
import { paramNanoLinkValidator } from "../middlewares/validatorManager.js";
const router = Router();

router.get("/:nanoLink", paramNanoLinkValidator, redirectNanoLink);

export default router;

redirect.controller.js

import { Link } from "../models/Link.js";

export const redirectNanoLink = async (req, res) => {
    try {
        const { nanoLink } = req.params;
        // console.log(nanoLink);
        const link = await Link.findOne({ nanoLink });

        if (!link)
            return res.status(404).json({ error: "No existe el nanoLink" });

        return res.redirect(link.longLink);
    } catch (error) {
        console.log(error);
        if (error.kind === "ObjectId") {
            return res.status(403).json({ error: "Formato id incorrecto" });
        }
        return res.status(500).json({ error: "error de servidor" });
    }
};

# mongo sanitize

# Cors

Levantar web public con liveserver para probar app

npm i cors
ORIGIN1=http://127.0.0.1:5500
import cors from "cors";

const app = express();

const whiteList = [process.env.ORIGIN1];
app.use(
    cors({
        origin: function(origin, callback) {
            if (whiteList.includes(origin)) {
                return callback(null, origin);
            }
            return callback("No autorizado por CORS");
        },
    })
);

# Deploy Heroku

Last Updated: 6/23/2022, 1:49:16 PM