# 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:
- Suscríbete al canal de Youtube (es gratis) click aquí (opens new window)
- Si estás viendo un video no olvides regalar un 👍 like y comentario 🙏🏼
- También puedes ser miembro del canal de Youtube click aquí (opens new window)
- 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,
];
# Links (URL)
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));
# getLinks
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;
# createLink
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" });
}
};
# removeLink
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" });
}
};
# updateLink
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" });
}
};
# get nanoLink (public)
// 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
- injection mongodb (opens new window)
- El problema radica en que se le pueda pasar un objeto a la consulta
{ $ne: 1 }
, Leer artículo aquí (opens new window) - Pero con Moongose nosotros hicimos un squema, por ende como definimos los campos como String, estos serán interpretados como tal, por ende no se ejecutará el objeto en cuestión.
# 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");
},
})
);