RESTApi desenvolvida em nodejs + mongodb + express
O projeto possui duas branchs, são:
- master: contém todo o desenvolvimento da API
- master-clean: contém os aquivos inicias da configuração do projeto(a base da api)
O ambiente de desenvolvimento foi configurado com .editorconfig + eslint.
Para isso, na raiz do projeto foi criado o arquivo .editorconfig com algumas configurações do editor.
Depois foi instalado o eslint yarn add eslit
. Logo após a instalação o eslint precisa ser configurado npx eslint --init
, onde foi selecionado as opções user a popular style guide, standard e JSON.
O servidor ./src/server.js possui a classe app, responsável por iniciar as rotas e middlewares do server:
const express = require("express");
class App {
constructor() {
this.express = express();
this.isDev = process.env.NODE_ENV !== "production";
this.middlewares();
this.routes();
}
middlewares() {
this.express.use(express.json());
}
routes() {
this.express.use(require("./routes"));
}
}
module.exports = new App().express;
O server é iniciado em ./index.js:
const server = require("./server");
server.listen(3000 || process.env.PORT);
Após configurar o ambiente, o banco de dados foi instalado e configurado com Docker, utilizando a imagem mongo sudo docker run --name mongonode -p 27017:27017 -d -t mongo
.
Feito isso, temos um banco de dados rodando na porta 27017.
Para manipulação do banco de dados pela aplicação, está sendo utilizado o ORM mongoose yarn add mongoose
.
Feito isso, em ./src/config foi criado o arquivo database.js, que contém a string de conexão com o banco.
A conexão com o mongo é feita pelo mongoose. Para isso o mongoose deve ser importado no server.js e configurado da seguinte forma:
database () {
mongoose.connect(databaseConfig.uri, {
useCreateIndex: true,
useNewUrlParser: true
})
}
Foi adicionado um hook na model User para criptografar a senha do usuário antes de ser salva no banco. Para a criptografia, foi utilizado o bcryptjs.
Obs. Hooks são operações realizadas na model antes que os dados sejam salvos, atualizdos, criados e/ou deletados do banco.
UserSchema.pre("save", async function(next) {
if (!this.isModified("password")) return next();
this.password = await bcrypt.hash(this.password, 8);
});
A Autenticação foi feita utilizando JWT(Json Web Token). Para isso, foi adicionado a controller SessionController e o método store().
Na model User, foi adicionado 2 métodos, são eles:
- compareHash: Método chamado para validar a senha na autenticação do usuário.
- generateToken: Caso o usuário passe na validação de e-mail e senha, é chamado o méotod generateToken({ user.id }), para retornar um token válido. O método é estático, por isso não necessita de uma instância da Classe User.
Para controlar as rotas seguras da aplicação, está sendo utilizado o auth middleware.
Aqui, básicamente ele recebe o token via header, captura o token e o valida.
Obs. Foi utilizado o { promisify } = require('util')
para transformar o jwt.verify em uma promisse, permitindo o uso do async await.
const jwt = require("jsonwebtoken");
const authConfig = require("../../config/auth");
const { promisify } = require("util");
module.exports = async (req, res, next) => {
const authHeader = req.headers.authorization;
if (!authHeader) {
return res.status(401).json({ error: "Token not provided" });
}
const [, token] = authHeader.split(" ");
try {
const decoded = await promisify(jwt.verify)(token, authConfig.secret);
req.userId = decoded.id;
return next();
} catch (err) {
return res.status(401).json({ error: "Token invalid" });
}
};
Para mais, consultar a documentação no arquivo ./app/middlewares/auth.js
.
Trazer todos os dados de uma collection pode resultar em problemas quando se tem uma grande quantidade de dados. Por isso, para paginar a aplicação foi utilizado o mongoose-paginate
.
Basta instalar yarn add mongoose-paginate
, ir na model que deseja paginar, no caso Ad, importar e adicionar o mongoose-paginate
.
const mongoose = require("mongoose");
const mongoosePaginate = require("mongoose-paginate");
const AdSchema = new mongoose.Schema();
AdSchema.plugin(mongoosePaginate);
module.exports = mongoose.model("Ad", AdSchema);
Feito isso, nas próximas requisições, podemos utilizar os métodos do mongoose-paginate da seguinte forma:
class AdController {
async index(req, res) {
const ads = await Ad.paginate(
{
/* FILTROS DO FIND()*/
},
{
limit: 20, // Limite por página
page: req.query.page || 1, // A pagima atual, normalmente vem em query.params
sort: "-createdAt", // Ordenação dos dados
populate: "author" // Para popular os relacionamentos da collection
}
);
return res.json(ads);
}
}
Para mais informações sobre a lib, consulte a documentação
Para envio de email está sendo utilizado o nodemailer
+ MailTrap. O MailTrap é um servidor SMTP onde todos os e-mails enviados caem na mesma caixa de entrada, usado em ambinete de desenvolvimento. Para produção, deve ser configurado um servidor externo como g-mail, mandril, Amazon Sas, ParkPost etc.
Para utilizar o nodemail, bastar instalar yarn add nodemailer
e configura-lo como serviço:
const nodemailer = require("nodemailer");
const transport = nodemailer.createTransport(
host: 'smtp.mailtrap.io',
port: '2525',
secure: false,
auth: {
user: '5a48bf600d3043',
pass: 'e6dffb5e723c4d'
}
});
module.exports = transport;
Para manter a organização do código, a configuração do método createTransport() pode ficar em um arquivo externo.
Feito a configuração do nodemailer, basta importa-lo e utilizar seus métodos em uma controller, exemplo:
// tranport
const Mail = require("../services/Mail");
Mail.sendMail({
from: '"Maicon Silva" <email@email.com>',
to: "com@com.br",
subject: "Solicitação de compra",
html: "<p>Test</p>"
});
A configuração de template possui duas dependências, são elas:
nodemailer-express-handlebars
: Configurações do nodemailerexpress-handlebars
: View engine
Após instalar as duas dependências, basta importa-las no arquivo de serviço do email Mail.js
e configurar da seguinte forma:
const path = require("path");
const hbs = require("nodemailer-express-handlebars");
const exphbs = require("express-handlebars");
const transport = nodemailer.createTransport(mailConfig);
// Configurações do template
transport.use(
"compile",
hbs({
viewEngine: exphbs(), // ViewEngine
viewPath: path.resolve(__dirname, "..", "views", "emails"), // Caminho das Views
extName: ".hbs" // Extensão das Views
})
);
Feito isso, na controller que enviará o e-mail, é necessário passar mais alguns parâmetros, ficando assim:
Mail.sendMail({
from: '"Maicon Silva" <maiconrs95@gmail.com>',
to: purchaseAd.author.email,
subject: `Solicitação de compra: ${purchaseAd.title}`,
template: "purchase", // Nome da View
context: { user, content, ad: purchaseAd } // Variáveis de template
});
return res.send();
Para que o usuário não necessite aguardar a reposta do envio do e-mail, que pode levar alguns segundos, foi implementando a fila. A fila uma operação que irá ser executada em segundo plano e quando estiver completa retornará uma resposta. Para isso vamos utilizar o redis, também através do docker sudo docker run --name noderedis -p 6379:6379 redis:alpine
.
O redis básicamente funciona com chaves que representam processos e quando "chamadas" executam um processo.
Para configurar o redis é necessário kue yarn add kue
.
Feito isso vamos configura-lo:
A pirmeira coisa foi remover o envio de e-mail do PurchaseController e passar a responsábilidade para um job jobs/PurchaseMail.js
.
Jobs serão operações executadas em segundo plano:
const Mail = require("../services/Mail");
// Job reponsável por enviar o email
class PurchaseMail {
// Retorna a chave única do redis
get keyof() {
return "PurchaseMail ";
}
// Serviço responsável por enviar o email
// job recebe todos os valores que serão passados para o job
// done é chamado quando o processo é concluído
async handle(job, done) {
const { ad, user, content } = job.data;
Mail.sendMail({
from: '"Maicon Silva" <maiconrs95@gmail.com>',
to: ad.author.email,
subject: `Solicitação de compra: ${ad.title}`,
template: "purchase",
context: { user, content, ad }
});
return done();
}
}
module.exports = new PurchaseMail();
Feito isso, nós criamos as chamadas de processos, passando o nome da fila e o processo a ser executado:
const kue = require("kue");
const redisConfig = require("../../config/redis");
const jobs = require("../jobs");
const Queue = kue.createQueue({
redis: {
host: "127.0.0.1",
port: 6379
}
});
/**
* @description: Inicia o processo redis passando a key é o método chamado
* Todos os processos que tiverem a mesma key serão iniciados na chamada
*/
Queue.process(jobs.PurchaseMail.key, jobs.PurchaseMail.handle);
module.exports = Queue;
Agora, em PurchaseController basta chamar o processo passando as váriaves com os valores do template:
// Executa e salva o job no redis
Queue.create(PurchaseMail.key, {
ad: { obj, data },
user: "User",
content: "Content"
}).save();
É isso. Para mais consulte os arquivos da pasta jobs.
Para validar os campos dos Schemas, foi utilizado a lib yarn add joi
. Ela basicamente reflete o Schema e valida os campos que não estão preenchidos corretamente.
Na utlização do Joi nós podemos validar tanto o body, params o os query params:
const Joi = require("joi");
/**
* @description: O Joi permite tanto a validação do body, params e query params
*/
module.exports = {
body: {
name: Joi.string().required(),
email: Joi.string()
.email()
.required(),
password: Joi.strict()
.required()
.min(6)
}
};
Para utilizar o Joi nas validações, é necessário a instalação do middleware express-validation yarn add express-validation
.
Feito isso, basta importar o express-validation e as validações feito pelo Joi no arquivo de rotas e configura-los como middleware da seguinte forma:
const validate = require("express-validation");
const validators = require("./app/validators");
/**
* User
*/
routes.post("/users", validate(validators.User), sua.controller);
As exception mostrará todos os possíveis erros na API. Para isso foi criado um método em Server.js que contém as configurações de error.
Obs. Para que a exception capture os erros nas rotas, o método exception() deve ser chamado depois do routes().
Como utilizamos o express-validation nas models, a API pode lançar um erro vindo dessa lib.
Para manipular essa exceção, basta validar no middleware que irá capturar os error da API, se o erro lançado é uma instância do express-validation:
const validator = require('express-validation')
exception () {
this.express.use((err, req, res, next) => {
// Valida se o erro lançado é uma instância do express-validation
if (err instanceof validatior.ValidationError) {
return res.status(err.status).json(err)
}
})
}
Para ter um acesso mais detalhado sobre o erro em ambiente de dev, foi utilizado a lib youch yarn add youch
.
Essa lib, básicamente, funciona como um formatador de erros.
Como os métodos das controllers estão declarados com async, eles passam a ser uma Promisse. E com isso, não irão disparar um erro a não ser que estejam em volto de um try catch(err). Para passar os erros dos métodos para o express, foi adicionado a lib express-async-handler.
Após a instalação da lib, basta adiciona-lá no arquivo de rotas e envolver a chamada das controllers na variável de import da lib:
const handler = require("express-async-handler");
routes.delete("/ads/:id", handler(controllers.AdController.destroy));
E agora no método exception() em server.js é possível lançar as exceções utilizando o Youch.
// Verifica se estamos em ambiente de desenvolvimento
if (process.env.NODE_ENV !== "production") {
const youch = new Youch(err, req);
return res.json(await youch.toJSON());
//return res.send(await youch.toHTML());
}
Para mais, consultar os arquivos routes.js e/ou Server.js
O Sentry é uma plataforma utilizada para capturar erros em produção, evitando que os usuários do sistema entrem ou percebam o erro.
Toda vez que a API lançar um erro, esse erro é enviado para o Sentry.
+Sentry
Após seguir a documentação, instalado o Sentry yarn add @sentry/node
, basta importa-lo e configurar o express() para receber os erros e enviar para o Sentry em ambiente de produção:
const Sentry = require('@sentry/node')
sentry () {
Sentry.init({ dsn: 'https://a849399c2e534922b14f568b3dee5ae0@sentry.io/1340291' })
}
exception () {
// Valida erros em produção
if (process.env.NODE_ENV === 'production') {
this.express.use(Sentry.Handlers.errorHandler())
}
...
}
Também é possível utilizar o sentry em filas, como no envio de e-mail. Basta importar o Sentry no service e configurar o Queue:
const Sentry = require("@sentry/node");
Queue.on("error", Sentry.captureException);
Variaveis de ambiente normalmente são diferentes em ambiente de produção x desenvolvimento. Para facilitar essa mudança, podemos centralizar a fonte desse valores, e altera-las dependendo do ambiente. Para isso, na raiz do projeto é necessário um arquivo .env, que contém os valores das variaveis dea ambiente:
NODE_ENV = development
APP_SECRET = GoNode2
DB_URL = mongodb://localhost:27017/gonode03
MAIL_HOST = smtp.mailtrap.io
MAIL_PORT = 2505
MAIL_USER = 5a48bf600d3043
MAIL_PASS = e6dffb5e723c4d
REDIS_HOST = 127.0.0.1
REDIS_PORT = 6379
SENTRY_DSN = https://a849399c2e534922b14f568b3dee5ae0@sentry.io/1340291
Feito isso, também é necessário a instalação da lib dotenv yarn add dotenv
.
Depois de instalada, deve ser chamada em server.js acima de todos os imports, para que toda a aplicação enxergue as variaveis:
require("dotenv").config(); // 1 linha do server.js
const express = require("express");
const mongoose = require("mongoose");
const databaseConfig = require("./config/database");
Feito, todas as variaveis de ambiente ficam disponível em process.env.CHAVE, e podem ser usada na aplicação:
module.exports = {
secret: process.env.APP_SECRET // CHAVE = valor setado no arquivo .env,
ttl: 86400
};
É isso. ❤️
Se você chegou até aqui é porque deve estar interessado no meu trabalho. Não perca tempo, entre em contato.
para mais informações > Maicon