En algún momento hable resumidamente de REST y por qué NO debe conservar un estado. No conservar un estado nos conlleva a un desacoplamiento entre aplicaciones y a una necesidad de implementar una capa de autorización, en este ejemplo hablaré precisamente de JWT.
La implementación es un desarrollo de una API REST básica con algunas funcionalidades interesantes en Fastify JS en su versión 4, un framework de Javascript orientado a la capa de servicios en backend. Posee un ecosistema amplio de funcionalidades que ya llevo un rato utilizando, este framework ofrece alta eficiencia y he notado reducciones de costos de infraestructura en la nube a comparación con otros marcos de trabajo de Javascript, si quieres conocer más de algunos benchmarks de este framework con algunos de sus competidores puedes revisarlo aquí.
Cabe recalcar que la API REST es completamente funcional, lo más parecido a un uso en aplicaciones de la vida real, es por eso que sentí la necesidad de utilizar un motor de almacenamiento en BD, para este caso utilicé MongoDB, un sistema de base de datos orientado a documentos (NoSQL).
También no menos importante, se genera una documentación de Swagger.
Si eres como yo y primero ves el código antes de recibir una explicación, puedes ver el repositorio.
Lo primero la estructura del proyecto, quiero transmitir el por qué las carpetas se organizaron de la siguiente manera:
src/config: Contiene información de configuración necesaria para iniciar los servicios, por ejemplo la url de conexión a mongoDB.
src/controllers: Siguiendo un paradigma de programación funcional, se declaran dentro de este directorio las acciones de la API, por ejemplo el archivo UsersController tendrá acciones exclusivamente de usuarios, esto indica que no deberá tener lógica funcional de nada más que no sea de usuarios (Como lo debe ser una clase de POO).
src/middlewares: Los middlewares actúan como un puente entre tecnologías o herramientas (librerías, funcionalidades, etc.) para que puedan integrarse, en este caso necesitamos un middleware de autorización.
src/models: Para este ejercicio se necesitó un motor de base de datos como lo mencioné anteriormente, MongoDB. Y para facilitar la interacción con la BD, necesitamos un ODM, un modelo de programación que nos da la posibilidad de mapear y manejar la información de los documentos, para este caso almacenados en Mongo. Existe una librería de javascript muy popular y funcional llamada mongoose, la mangosta es una excelente opción de ODM. Teniendo este contexto un poco resumido en src/models se almacenan los Schemas necesarios para la API.
src/routes: Sí leiste mi explicación de API REST notaste que hable de los verbos y recursos, aquí definimos los recursos que necesita nuestro servicio.
Para iniciar los servicios con docker-compose habrá que ejecutar:
docker-compose up
Tengo que indicar que los servicios declarados son los siguientes:
version: '3.4'
services:
fastify-api:
build:
context: .
dockerfile: Dockerfile
target: dev
restart: "no"
environment:
NODE_ENV: development
ports:
- 3001:3001
volumes:
- .:/app
- node_modules:/app/node_modules
networks:
- fastify-example-network
mongo-bd:
image: mongo
restart: "no"
ports:
- 27017:27017
volumes:
- dbvolume:/data
environment: {
AUTH: "no"
}
networks:
- fastify-example-network
#Networks
networks:
fastify-example-network:
driver: bridge
volumes:
node_modules:
dbvolume:
driver: local
En el contexto principal tenemos a fastify-api encargado de dar de alta el servicio de la API REST, el siguiente servicio es el de mongo-bd una instancia de la imagen actual de mongo en docker registry, esto con la finalidad de servir el motor de base de datos a la API REST en un networking local.
El Dockerfile del servicio principal:
FROM node:16.16-alpine as base
# set working directory
WORKDIR /app
# install app dependencies
COPY package*.json ./
RUN npm install
COPY . ./
FROM base as dev
# start app
CMD ["npm", "run", "start:dev"]
FROM base as prod
# build
RUN npm build
# start app
CMD ["npm", "run", "start:prod"]
Instancia de una imagen base de node:16.16-alpine planteo el multistage basado en dev y prod, para fines prácticos en el archivo docker-compose.yml se hace target al step dev.
Configuración
Existe el archivo src/config/index.js que se encarga de gestionar algunas variables importantes:
module.exports = {
env: "development",
port: 3001,
host: "0.0.0.0",
db_url: "mongodb://mongo-bd:27017",
jwt_secret: "mi_secret",
jwt_expire: "8h",
jwt_issuer: "localhost",
};
Quiero mencionar que el manejo de estás variables deberían ser gestionadas por variables de entorno para escenarios productivos, el hacerlo aquí no está del todo mal, por que abstraemos en un lugar preciso donde radican estás variables, pero cuando exista un código productivo, nunca deberán vivir en el repositorio de código, por ejemplo ese mismo archivo se sustituirá:
module.exports = {
env: process.env.ENVIRONMENT,
port: process.env.PORT,
host: process.env.HOST,
db_url: process.env.DB_URL,
jwt_secret: process.env.JWT_SECRET,
jwt_expire: process.env.JWT_EXPIRE,
jwt_issuer: process.env.JWT_ISSUER,
};
Para fines prácticos de este ejercicio tomaremos la opción inicial.
Capa de datos, Schemas y Models
Para entender la capa de datos explicaré los modelos que son base de los documentos que maneja esta API:
El modelo USER en: src/models/User.js
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const UserSchema = new mongoose.Schema(
{
email: {
type: String,
required: true,
index: { unique: true },
validate: [
function (email) {
return email.match(
/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/
);
},
"email is invalid",
],
},
password: {
type: String,
required: true,
},
firstname: {
type: String,
required: true,
},
lastname: {
type: String,
required: true,
},
},
{
timestamps: {
createdAt: "created_at",
updatedAt: "updated_at",
},
}
);
// generando hash de la contraseña
UserSchema.pre("save", function (next) {
const user = this;
// si no es cambio de contraseña, modificar
if (!user.isModified("password")) return next();
const saltFact = parseInt(process.env.AUTH_SALT_FACTOR);
// generar salt
bcrypt.genSalt(saltFact, function (err, salt) {
if (err) return next(err);
// hash usando salt generada
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
// override
user.password = hash;
next();
});
});
});
// remover password
UserSchema.set("toJSON", {
transform: function (doc, user, opt) {
delete user.password;
return user;
},
});
module.exports = mongoose.model("User", UserSchema);
Este modelo de mongoose, se definen los siguientes atributos:
Campo | Tipo | Requerido | Unico | Custom validation |
---|---|---|---|---|
String | * | * | Validate email | |
password | String | * | ||
firstname | String | * | ||
lastname | String | * | ||
created_at | timestamp | |||
updated_at | timestamp |
La capa de datos será siempre dictada por las necesidades del negocio, por ejemplo si una necesidad para el software es almacenar el user-agent del usuario, en este modelo deberá definirse este campo. Para este fin de desarrollar una API sencilla, los campos que tenemos son funcionales.
En este punto se pueden definir hooks, como se observa existe el hook pre-save que nos resuelve una necesidad de todo software productivo, no guardar claves sin encriptación en una base de datos. Por ejemplo el usuario juanito, modifica su contraseña con el string: “contraseña”, esta cadena de caracteres nunca debe guardarse en base de datos tal cual, si no, debe guardarse mediante un algoritmo de encriptación, ya sea de uno o dos sentidos, para este caso utilice bcrypt.
El objetivo de este hook es para que antes de guardar a un usuario, si existe una modificación de contraseña (Nueva contraseña o actualización), sustituya el string por un hash generado por bcrypt y ese hash almacenarlo en base de datos.
UserSchema.pre("save", function (next) {
const user = this;
// si no es cambio de contraseña, modificar
if (!user.isModified("password")) return next();
const saltFact = parseInt(process.env.AUTH_SALT_FACTOR);
// generar salt
bcrypt.genSalt(saltFact, function (err, salt) {
if (err) return next(err);
// hash usando salt generada
bcrypt.hash(user.password, salt, function (err, hash) {
if (err) return next(err);
// override
user.password = hash;
next();
});
});
});
Otra funcionalidad importante para este modelo es la conversión del documento a JSON, en este bloque de código observamos:
// remover password
UserSchema.set("toJSON", {
transform: function (doc, user, opt) {
delete user.password;
return user;
},
});
Esto para que cuando se realice la serialización que es prácticamente todas las respuestas de la API, elimine el campo contraseña (visualmente), algo importantísimo por que no queremos andar exponiendo hashes a lo desgraciado.
El modelo products en: src/models/Product.js
const mongoose = require("mongoose");
const ProductSchema = new mongoose.Schema(
{
code: String,
title: {
type: String,
required: true,
},
description: String,
price: Number,
quantity: Number,
user_id: {
type: mongoose.Schema.Types.ObjectId,
ref: "User",
required: true,
index: true,
},
},
{
timestamps: {
createdAt: "created_at",
updatedAt: "updated_at",
},
}
);
module.exports = mongoose.model("Product", ProductSchema);
Recordemos que es una API sencilla pero necesitaba algo para que no no sea una API de usuarios simplemente, en este punto se define un modelo encargado de la capa de productos, con las siguientes características:
Campo | Tipo | Requerido |
---|---|---|
code | String | |
title | String | * |
description | String | |
price | Number | |
quantity | Number | |
user_id | ObjectId | * |
created_at | timestamp | |
updated_at | timestamp |
El campo user_id será el encargado de administrar los productos por usuario.
Controladores
Siguiendo un patrón patrón de arquitectura MVC con mucha injerencia en mí, se plantea mantener en los controladores la reutilización de código, la separación de conceptos y funcionalidad de los modelos de Users y Products.
UserController en: src/controllers/UserController.js
const User = require("../models/User");
const boom = require("@hapi/boom");
const bcrypt = require("bcrypt");
async function auth(req, res) {
try {
const { email, password } = req.body;
const user = await User.findOne({ email: email });
// si no existe user
if (!user) {
return res.code(401).send("user email not found");
}
// comparar contraseña
const isMatch = await bcrypt.compare(password, user.password);
// si es contraseña incorrecta
if (!isMatch) {
return res.code(401).send("password not match");
}
// get token
const token = await res.jwtSign({ _id: user._id });
return {
token: token,
user: user,
};
} catch (err) {
throw boom.boomify(err);
}
}
async function register(req, res) {
try {
const body = req.body;
// register user
const user = await User.create(body);
// set token
const token = await res.jwtSign({ _id: user._id });
// response user and toker
return {
token: token,
user: user,
};
} catch (err) {
throw boom.boomify(err);
}
}
async function update(req, res) {
try {
const user = await User.findById(req.user._id).exec();
const update = req.body;
if (update.firstname) {
user.firstname = update.firstname;
}
if (update.lastname) {
user.lastname = update.lastname;
}
await user.validate();
return user.save();
} catch (err) {
throw boom.boomify(err);
}
}
async function changePassword(req, res) {
try {
const user = await User.findById(req.user._id).exec();
user.password = req.body.password;
await user.validate();
return user.save();
} catch (err) {
throw boom.boomify(err);
}
}
module.exports = {
auth,
register,
changePassword,
update,
};
Este controlador declará las funcionalidades sobre el modelo de usuarios:
- Autenticar a un usuario.
- Registrar a un nuevo usuario.
- Cambiar la contraseña de un usuario.
- Actualizar datos un usuario.
ProductController en: src/controllers/ProductController.js
const Product = require("../models/Product");
const boom = require("@hapi/boom");
async function getAll(req, res) {
try {
return Product.find({
user_id: req.user._id,
}).exec();
} catch (err) {
throw boom.boomify(err);
}
}
async function get(req, res) {
try {
return Product.findById(req.params._id).exec();
} catch (err) {
throw boom.boomify(err);
}
}
async function save(req, res) {
try {
const data = req.body;
data.user_id = req.user._id;
const product = new Product(data);
return product.save();
} catch (err) {
throw boom.boomify(err);
}
}
async function update(req, res) {
try {
// filtrar por id y user_id
// evitando que otro token solicite
// actualizar recursos de otro usuario
const filter = { _id: req.params._id, user_id: req.user._id };
const update = req.body;
// setear user_id del token, evitando la
// modificación de user id por la solicitud
update.user_id = req.user._id;
return Product.findOneAndUpdate(filter, update, {
returnOriginal: false,
});
} catch (err) {
throw boom.boomify(err);
}
}
async function remove(req, res) {
try {
// filtrar por id y user_id
// evitando que otro token solicite
// remover recursos de otro usuario
const filter = { _id: req.params._id, user_id: req.user._id };
return Product.deleteOne(filter);
} catch (err) {
throw boom.boomify(err);
}
}
module.exports = {
save,
update,
remove,
get,
getAll,
};
Este controlador declara las funcionalidades sobre el modelo de productos:
- Guardar un nuevo producto.
- Actualizar un producto.
- Eliminar un producto.
- Obtener un producto.
- Obtener todos los productos de un usuario.
Middlewares
Para este ejercicio existe un middleware simple encargado de la verificación del token JWT.
module.exports = async function (request, reply) {
try {
await request.jwtVerify();
} catch (err) {
return reply.code(401).send("token expired or invalid");
}
};
Rutas
Las rutas están definidas en el archivo src/routes/index.js:
const Product = require("../controllers/ProductController");
const User = require("../controllers/UserController");
const AuthMiddleware = require("../middlewares/AuthMiddleware");
module.exports = [
{
method: "POST",
url: "/user/auth",
handler: User.auth,
schema: {
description:
"Este endpoint es utilizado para identificar un usuario con email y contraseña",
tags: ["User Auth"],
body: {
description: "Credenciales",
type: "object",
properties: {
email: { type: "string" },
password: { type: "string" },
},
},
response: {
200: {
description: "Response success",
type: "object",
properties: {
token: { type: "string" },
user: {
type: "object",
properties: {
_id: { type: "string" },
email: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
created_at: { type: "string" },
updated_at: { type: "string" },
},
},
},
},
},
},
},
{
method: "POST",
url: "/user/register",
handler: User.register,
schema: {
description:
"Este endpoint es utilizado para registrar un nuevo usuario que no exista anteriormente",
tags: ["User Auth"],
body: {
type: "object",
properties: {
email: { type: "string" },
password: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
},
},
response: {
200: {
description: "Response success",
type: "object",
properties: {
token: { type: "string" },
user: {
type: "object",
properties: {
_id: { type: "string" },
email: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
created_at: { type: "string" },
updated_at: { type: "string" },
},
},
},
},
},
},
},
{
method: "PUT",
url: "/user/password",
handler: User.changePassword,
preValidation: AuthMiddleware,
schema: {
description:
"Este endpoint se utiliza para modificar la contraseña del usuario",
tags: ["User"],
body: {
type: "object",
properties: {
email: { type: "string" },
},
},
response: {
200: {
description: "Response success",
type: "object",
properties: {
_id: { type: "string" },
email: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
created_at: { type: "string" },
updated_at: { type: "string" },
},
},
},
security: [
{
Bearer: [],
},
],
},
},
{
method: "PUT",
url: "/user",
handler: User.update,
preValidation: AuthMiddleware,
schema: {
description:
"Este endpoint se utiliza para modificar los datos del usuario",
tags: ["User"],
body: {
type: "object",
properties: {
firstname: { type: "string" },
lastname: { type: "string" },
},
},
response: {
200: {
description: "Response success",
type: "object",
properties: {
_id: { type: "string" },
email: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
created_at: { type: "string" },
updated_at: { type: "string" },
},
},
},
security: [
{
Bearer: [],
},
],
},
},
// products
{
method: "GET",
url: "/product",
handler: Product.getAll,
preValidation: AuthMiddleware,
schema: {
description:
"Este endpoint se utiliza para obtener todos los productos del usuario",
tags: ["Product"],
response: {
200: {
description: "Response success",
type: "array",
items: {
type: "object",
properties: {
_id: { type: "string" },
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
user_id: { type: "string" },
},
},
},
},
security: [
{
Bearer: [],
},
],
},
},
{
method: "GET",
url: "/product/:_id",
handler: Product.get,
preValidation: AuthMiddleware,
schema: {
params: {
type: "object",
properties: {
_id: {
type: "string",
description: "product id",
},
},
},
description: "Este endpoint se utiliza para obtener un producto por id",
tags: ["Product"],
response: {
200: {
description: "Response success",
type: "object",
properties: {
_id: { type: "string" },
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
user_id: { type: "string" },
},
},
},
security: [
{
Bearer: [],
},
],
},
},
{
method: "POST",
url: "/product",
handler: Product.save,
preValidation: AuthMiddleware,
schema: {
description: "Este endpoint se utiliza para crear un nuevo producto",
tags: ["Product"],
body: {
type: "object",
properties: {
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
},
},
response: {
200: {
description: "Response success",
type: "object",
properties: {
_id: { type: "string" },
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
user_id: { type: "string" },
},
},
},
security: [
{
Bearer: [],
},
],
},
},
{
method: "PUT",
url: "/product/:_id",
handler: Product.update,
preValidation: AuthMiddleware,
schema: {
params: {
type: "object",
properties: {
_id: {
type: "string",
description: "product id",
},
},
},
description: "Este endpoint se utiliza para modificar un producto por id",
tags: ["Product"],
body: {
type: "object",
properties: {
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
},
},
response: {
200: {
description: "Response success",
type: "object",
properties: {
_id: { type: "string" },
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
user_id: { type: "string" },
},
},
},
security: [
{
Bearer: [],
},
],
},
},
{
method: "DELETE",
url: "/product/:_id",
handler: Product.remove,
preValidation: AuthMiddleware,
schema: {
params: {
type: "object",
properties: {
_id: {
type: "string",
description: "product id",
},
},
},
description: "Este endpoint se utiliza para eliminar un producto por id",
tags: ["Product"],
security: [
{
Bearer: [],
},
],
},
},
];
Observamos que se declaran rutas para exponer los recursos de usuarios y productos.
Dentro de la definición de estás rutas se crean Schemas para cada una de ellas, con esto alimentar la documentación de swagger.
- Aunque no es obligación, Fastify siempre recomienda validar y serializar las rutas con JSON Schema.
Iniciar el servidor
Lo se, ya se hizo un poco largo pero ya viene la parte casi final, iniciar el servidor para exponer los recursos:
const Fastify = require("fastify");
const mongoose = require("mongoose");
const routes = require("./routes");
const config = require("./config");
const fastify = Fastify({
logger: true,
});
// habilitamos jwt
fastify.register(require("@fastify/jwt"), {
secret: config.jwt_secret,
sign: {
expiresIn: config.jwt_expire,
issuer: config.jwt_issuer,
},
verify: {
issuer: config.jwt_issuer,
},
});
// habilitamos cors
fastify.register(require("@fastify/cors"), {
origin: function (origin, cb) {
// por el momento todas las peticiones pasan
// en un escenario productivo esto debe controlarse
if (config.env === "development") {
return cb(null, true);
}
// Error access
cb(new Error(`Not allowed for ${origin}`));
},
});
(async function () {
try {
await mongoose.connect(config.db_url, {
autoIndex: config.env === "development" ? true : false,
});
} catch (error) {
fastify.log.error("Error to connect mongodb: ", error);
process.exit(1);
}
// es necesario esperar el callback de swagger
await fastify.register(require("@fastify/swagger"), {
routePrefix: "/swagger",
swagger: {
info: {
title: "Test API - Fastify, Mongo, Docker, Swagger",
description: "Fastify swagger API",
version: "0.1.0",
},
host: `${config.host}:${config.port}`,
schemes: ["http"],
consumes: ["application/json"],
produces: ["application/json"],
definitions: {
User: {
type: "object",
required: ["email", "firstname", "lastname"],
properties: {
_id: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
email: { type: "string", format: "email" },
created_at: { type: "Date" },
updated_at: { type: "Date" },
},
},
Product: {
type: "object",
required: ["title", "user_id"],
properties: {
_id: { type: "string" },
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
user_id: { type: "string" },
},
},
},
securityDefinitions: {
Bearer: {
type: "apiKey",
name: "Authorization",
in: "header",
},
},
},
exposeRoute: true,
});
// arma rutas posterior a cargar swaggar
routes.forEach((route) => {
fastify.route(route);
});
fastify.listen({ port: config.port, host: config.host }, function (err, _) {
if (err) {
fastify.log.error(err);
process.exit(1);
}
});
})();
Algo similar que tienen Fastify a Express es el manejo de módulos como middlewares de nivel aplicación, utilizando una utilidad llamada use. Con esto registramos los siguientes módulos @fastify/cors y @fastify/jwt. El primero se encarga de validar CORS y el segundo es el encargado de exponer la funcionalidad para el manejo de JSON Web Tokens (JWT).
La siguiente funciona anónima:
(async function () {
try {
await mongoose.connect(config.db_url, {
autoIndex: config.env === "development" ? true : false,
});
} catch (error) {
fastify.log.error("Error to connect mongodb: ", error);
process.exit(1);
}
// es necesario esperar el callback de swagger
await fastify.register(require("@fastify/swagger"), {
routePrefix: "/swagger",
swagger: {
info: {
title: "Test API - Fastify, Mongo, Docker, Swagger",
description: "Fastify swagger API",
version: "0.1.0",
},
host: `${config.host}:${config.port}`,
schemes: ["http"],
consumes: ["application/json"],
produces: ["application/json"],
definitions: {
User: {
type: "object",
required: ["email", "firstname", "lastname"],
properties: {
_id: { type: "string" },
firstname: { type: "string" },
lastname: { type: "string" },
email: { type: "string", format: "email" },
created_at: { type: "Date" },
updated_at: { type: "Date" },
},
},
Product: {
type: "object",
required: ["title", "user_id"],
properties: {
_id: { type: "string" },
code: { type: "string" },
title: { type: "string" },
description: { type: "string" },
price: { type: "number" },
quantity: { type: "number" },
user_id: { type: "string" },
},
},
},
securityDefinitions: {
Bearer: {
type: "apiKey",
name: "Authorization",
in: "header",
},
},
},
exposeRoute: true,
});
// arma rutas posterior a cargar swaggar
routes.forEach((route) => {
fastify.route(route);
});
fastify.listen({ port: config.port, host: config.host }, function (err, _) {
if (err) {
fastify.log.error(err);
process.exit(1);
}
});
})();
Con el uso de async/await nos decidimos a esperar a swagger y a mongoose por su naturaleza asíncrona, si no hacemos esto, nos arriesgamos a que la documentación nunca sería visualizada en el caso de swagger y para moongose si llega a existir un error en la conexión con la BD, por ejemplo que no esté disponible o una URL mal, nunca levantará el servidor, es la manera de controlar los errores antes de iniciar los servicios.
Quedará armar las rutas que definimos anteriormente:
// arma rutas posterior a cargar swaggar
routes.forEach((route) => {
fastify.route(route);
});
y escuchar las peticiones con el servidor iniciado:
fastify.listen({ port: config.port, host: config.host }, function (err, _) {
if (err) {
fastify.log.error(err);
process.exit(1);
}
});
Testing
Para probar la API utilice Postman, así que empezamos a consumir recursos mediante la UI de Postman. Las acciones que probamos son las definidas en las rutas.
Registro
Necesitamos como primer paso, un nuevo usuario.
Method / URL
POST / http://localhost:3001/user/register
Body
{
"email": "example@emailexample.com",
"password": "contraseña",
"firstname": "Juan",
"lastname": "Peréz"
}
Response
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzE0ZTkyMWRmZTJlNTRhZWVkNWFiNzYiLCJpYXQiOjE2NjIzMTQ3ODUsImV4cCI6MTY2MjM0MzU4NX0.QygI4knbe2QV-pdzPFldWlVYWnNxXUmaTc1kW50vx7o",
"user": {
"_id": "6314e921dfe2e54aeed5ab76",
"email": "example@emailexample.com",
"firstname": "Juan",
"lastname": "Peréz",
"created_at": "2022-09-04T18:06:25.786Z",
"updated_at": "2022-09-04T18:06:25.786Z"
}
}
La respuesta nos devuelve el token JWT: eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJfaWQiOiI2MzE0ZTkyMWRmZTJlNTRhZWVkNWFiNzYiLCJpYXQiOjE2NjIzMTQ3ODUsImV4cCI6MTY2MjM0MzU4NX0.QygI4knbe2QV-pdzPFldWlVYWnNxXUmaTc1kW50vx7o, este token es necesario para empezar a utilizar los demas recursos que tienen un preValidation definido en las rutas.
- Cabe recalcar que el token nunca es similar, así que si pruebas esto deberas sustituir el token por el que te proporcionó la API.
Cambio de contraseña
Si queremos cambiar la contraseña se necesita solicitar el siguiente recurso:
Method / URL
PUT / http://localhost:3001/user/password
Body
{
"password": "nuevacontraseña"
}
La respuesta será la siguiente si no colocamos el token en las cabeceras.
token expired or invalid
Para que la API nos autorice solicitar el recurso se debe colocar un Bearer token dentro de la pestaña de Authorization:
De esta manera ya saltamos la validación de token correctamente y nos permitirá actualizar la contraseña:
Editar usuario
El recurso para actualizar los datos del usuario es el siguiente:
Method / URL
PUT / http://localhost:3001/user
Body
{
"firstname": "Juanito",
"lastname": "Martínez"
}
Response
{
"_id": "6314e921dfe2e54aeed5ab76",
"email": "example@emailexample.com",
"firstname": "Juanito",
"lastname": "Martínez",
"created_at": "2022-09-04T18:06:25.786Z",
"updated_at": "2022-09-04T18:57:15.932Z"
}
Crear producto
Method / URL
POST / http://localhost:3001/product
Body
{
"code": "SKU-1274",
"title": "Teclado",
"description": "Teclado mecánico 60%",
"price": 80.50,
"quantity": 30
}
Response
{
"_id": "6314f634dfe2e54aeed5ab7c",
"code": "SKU-1274",
"title": "Teclado",
"description": "Teclado mecánico 60%",
"price": 80.5,
"quantity": 30,
"user_id": "6314e921dfe2e54aeed5ab76"
}
Actualizar producto
Este recibe el id del recurso que debe actualizar.
Method / URL
PUT / http://localhost:3001/product/6314f634dfe2e54aeed5ab7c
Body
{
"description": "Teclado mecánico 60% RGB",
"price": 90.50,
"quantity": 25
}
Response
{
"_id": "6314f634dfe2e54aeed5ab7c",
"code": "SKU-1274",
"title": "Teclado",
"description": "Teclado mecánico 60% RGB",
"price": 90.5,
"quantity": 25,
"user_id": "6314e921dfe2e54aeed5ab76"
}
Obtener producto
Method / URL
GET / http://localhost:3001/product/6314f634dfe2e54aeed5ab7c
Response
{
"_id": "6314f634dfe2e54aeed5ab7c",
"code": "SKU-1274",
"title": "Teclado",
"description": "Teclado mécanico 60% RGB",
"price": 90.5,
"quantity": 25,
"user_id": "6314e921dfe2e54aeed5ab76"
}
Obtener todos los productos del usuario
Method / URL
GET / http://localhost:3001/product
Response
[
{
"_id": "6314f634dfe2e54aeed5ab7c",
"code": "SKU-1274",
"title": "Teclado",
"description": "Teclado mecánico 60% RGB",
"price": 90.5,
"quantity": 25,
"user_id": "6314e921dfe2e54aeed5ab76"
}
]
Si observamos la respuesta es un listado de productos que el usuario ha creado, en este caso de ejemplo solo hemos creado un producto.
Eliminar producto
Method / URL
DELETE / http://localhost:3001/product/6314f634dfe2e54aeed5ab7c
Response
{
"acknowledged": true,
"deletedCount": 1
}
Documentación
La documentación puede ser consultada en la siguiente URL. Esto nos da detalles de todos los recursos disponibles y nos coloca ejemplos para poder entender mejor la API. Podremos ejecutar las mismas pruebas que en Postman con el botón “Try out”.
Por ejemplo, la autenticación:
Cabe recalcar que todos los recursos que tienen un candado, son protegidos por acceso con un token JWT:
De igual manera que en Postman será necesario colocar este valor en Authorize:
Se hizo un poco largo pero estoy seguro que te ayudará a entender mejor el uso de JWT, te invito a probar más a fondo la API desde swagger.