Chegamos ao fim do Módulo 4. Em seis artigos você saiu do zero no Node.js até construir APIs com banco de dados real. Agora vamos revisar tudo e construir o projeto final do módulo — uma API REST completa com autenticação JWT, unindo Node, Express, MongoDB e Mongoose em uma aplicação profissional.
Revisão rápida — Módulo 4
Node.js
// Ambiente de execução JavaScript fora do navegador
// Motor V8 — mesmo do Chrome
// Acesso ao sistema operacional via módulos nativos
const fs = require("fs/promises");
const path = require("path");
const os = require("os");
// process — ponte com o sistema
process.env.PORT; // variáveis de ambiente
process.argv.slice(2); // argumentos da CLI
process.exit(1); // encerra com erro
// Módulos: CommonJS (require) ou ES Modules (import)
NPM e package.json
{
"scripts": {
"start": "node src/index.js",
"dev": "nodemon src/index.js"
},
"dependencies": { "express": "^4.18.2" },
"devDependencies": { "nodemon": "^3.0.2" }
}
npm install # instala dependências
npm ci # instala versões exatas (produção/CI)
npm run dev # executa script "dev"
npm audit # verifica vulnerabilidades
# Nunca commite node_modules — sempre commite package-lock.json
Servidor HTTP e Express
// Express abstrai o módulo http
const express = require("express");
const app = express();
app.use(express.json()); // parse de JSON
app.use(logger); // middleware customizado
app.get("/rota/:id", handler); // rota com parâmetro
app.post("/rota", handler); // POST
// Error handler — sempre 4 parâmetros
app.use((erro, req, res, next) => {
res.status(500).json({ erro: erro.message });
});
app.listen(3000);
MongoDB e Mongoose
// Schema → Model → Operações CRUD
const schema = new Schema({ nome: { type: String, required: true } });
const Model = mongoose.model("Nome", schema);
await Model.create(dados);
await Model.find(filtros).sort("-createdAt").limit(10);
await Model.findByIdAndUpdate(id, { $set: dados }, { new: true });
await Model.findByIdAndDelete(id);
// Índices, virtuals, hooks (pre/post), métodos, populate
O projeto — API REST com JWT
Vamos construir uma API de gerenciamento de tarefas com:
- Cadastro e login de usuários com hash de senha (bcrypt)
- Autenticação via JWT (JSON Web Token)
- CRUD de tarefas vinculadas ao usuário logado
- Proteção de rotas por token
- Paginação e filtros
- Tratamento completo de erros
Estrutura do projeto
src/
├── config/
│ └── database.js
├── models/
│ ├── Usuario.js
│ └── Tarefa.js
├── middlewares/
│ ├── auth.js
│ ├── erros.js
│ └── logger.js
├── controllers/
│ ├── authController.js
│ └── tarefaController.js
├── routes/
│ ├── auth.js
│ └── tarefas.js
└── index.js
npm init -y
npm install express mongoose dotenv bcrypt jsonwebtoken
npm install -D nodemon
# .env
PORT=3000
NODE_ENV=development
MONGODB_URL=mongodb://localhost:27017/tarefas-api
JWT_SECRET=sua-chave-secreta-muito-longa-e-aleatoria-aqui
JWT_EXPIRA_EM=7d
Models
// src/models/Usuario.js
const mongoose = require("mongoose");
const bcrypt = require("bcrypt");
const usuarioSchema = new mongoose.Schema(
{
nome: {
type: String,
required: [true, "Nome é obrigatório."],
trim: true,
minlength: [2, "Nome deve ter pelo menos 2 caracteres."],
},
email: {
type: String,
required: [true, "Email é obrigatório."],
unique: true,
lowercase: true,
trim: true,
match: [/^S+@S+.S+$/, "Email inválido."],
},
senha: {
type: String,
required: [true, "Senha é obrigatória."],
minlength: [6, "Senha deve ter pelo menos 6 caracteres."],
select: false, // nunca retorna a senha
},
ativo: { type: Boolean, default: true },
},
{ timestamps: true, versionKey: false }
);
// Hash da senha antes de salvar
usuarioSchema.pre("save", async function (next) {
if (!this.isModified("senha")) return next();
this.senha = await bcrypt.hash(this.senha, 12);
next();
});
// Método para verificar senha
usuarioSchema.methods.verificarSenha = async function (senhaDigitada) {
return bcrypt.compare(senhaDigitada, this.senha);
};
// Remove campos sensíveis da resposta JSON
usuarioSchema.methods.toJSON = function () {
const obj = this.toObject();
delete obj.senha;
obj.id = obj._id;
delete obj._id;
return obj;
};
module.exports = mongoose.model("Usuario", usuarioSchema);
// src/models/Tarefa.js
const mongoose = require("mongoose");
const tarefaSchema = new mongoose.Schema(
{
titulo: {
type: String,
required: [true, "Título é obrigatório."],
trim: true,
maxlength: [200, "Título não pode exceder 200 caracteres."],
},
descricao: {
type: String,
trim: true,
maxlength: [2000, "Descrição não pode exceder 2000 caracteres."],
default: "",
},
status: {
type: String,
enum: {
values: ["pendente", "em_progresso", "concluida", "cancelada"],
message: "Status inválido.",
},
default: "pendente",
},
prioridade: {
type: String,
enum: ["baixa", "media", "alta"],
default: "media",
},
prazo: {
type: Date,
default: null,
},
tags: [String],
usuario: {
type: mongoose.Schema.Types.ObjectId,
ref: "Usuario",
required: true,
},
},
{ timestamps: true, versionKey: false }
);
// Índices
tarefaSchema.index({ usuario: 1, status: 1 });
tarefaSchema.index({ usuario: 1, createdAt: -1 });
// Virtual — indica se está atrasada
tarefaSchema.virtual("atrasada").get(function () {
if (!this.prazo || this.status === "concluida") return false;
return new Date() > this.prazo;
});
tarefaSchema.set("toJSON", { virtuals: true });
module.exports = mongoose.model("Tarefa", tarefaSchema);
Middleware de autenticação JWT
// src/middlewares/auth.js
const jwt = require("jsonwebtoken");
const Usuario = require("../models/Usuario");
async function autenticar(req, res, next) {
try {
// 1. Extrai o token do header
const authHeader = req.headers.authorization;
if (!authHeader?.startsWith("Bearer ")) {
return res.status(401).json({ erro: "Token de autenticação ausente." });
}
const token = authHeader.split(" ")[1];
// 2. Verifica e decodifica o token
let payload;
try {
payload = jwt.verify(token, process.env.JWT_SECRET);
} catch (erro) {
if (erro.name === "TokenExpiredError") {
return res.status(401).json({ erro: "Token expirado. Faça login novamente." });
}
return res.status(401).json({ erro: "Token inválido." });
}
// 3. Busca o usuário no banco
const usuario = await Usuario.findById(payload.id);
if (!usuario || !usuario.ativo) {
return res.status(401).json({ erro: "Usuário não encontrado ou inativo." });
}
// 4. Anexa o usuário ao req
req.usuario = usuario;
next();
} catch (erro) {
next(erro);
}
}
module.exports = { autenticar };
Controllers
// src/controllers/authController.js
const jwt = require("jsonwebtoken");
const Usuario = require("../models/Usuario");
function gerarToken(id) {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: process.env.JWT_EXPIRA_EM || "7d",
});
}
// POST /auth/registrar
async function registrar(req, res, next) {
try {
const { nome, email, senha } = req.body;
// Verifica se email já existe
const existente = await Usuario.findOne({ email });
if (existente) {
return res.status(409).json({ erro: "Email já cadastrado." });
}
const usuario = await Usuario.create({ nome, email, senha });
const token = gerarToken(usuario._id);
res.status(201).json({
mensagem: "Usuário criado com sucesso.",
token,
usuario,
});
} catch (erro) {
next(erro);
}
}
// POST /auth/login
async function login(req, res, next) {
try {
const { email, senha } = req.body;
if (!email || !senha) {
return res.status(400).json({ erro: "Email e senha são obrigatórios." });
}
// Busca o usuário com a senha (select: false no schema)
const usuario = await Usuario.findOne({ email }).select("+senha");
if (!usuario) {
return res.status(401).json({ erro: "Credenciais inválidas." });
}
// Verifica a senha
const senhaCorreta = await usuario.verificarSenha(senha);
if (!senhaCorreta) {
return res.status(401).json({ erro: "Credenciais inválidas." });
}
if (!usuario.ativo) {
return res.status(401).json({ erro: "Conta desativada." });
}
const token = gerarToken(usuario._id);
// Remove a senha antes de enviar
usuario.senha = undefined;
res.json({
mensagem: "Login realizado com sucesso.",
token,
usuario,
});
} catch (erro) {
next(erro);
}
}
// GET /auth/eu
async function eu(req, res) {
// req.usuario já foi preenchido pelo middleware autenticar
res.json({ usuario: req.usuario });
}
// PATCH /auth/senha
async function alterarSenha(req, res, next) {
try {
const { senhaAtual, novaSenha } = req.body;
if (!senhaAtual || !novaSenha) {
return res.status(400).json({ erro: "senhaAtual e novaSenha são obrigatórias." });
}
if (novaSenha.length < 6) {
return res.status(422).json({ erro: "Nova senha deve ter pelo menos 6 caracteres." });
}
const usuario = await Usuario.findById(req.usuario._id).select("+senha");
const senhaCorreta = await usuario.verificarSenha(senhaAtual);
if (!senhaCorreta) {
return res.status(401).json({ erro: "Senha atual incorreta." });
}
usuario.senha = novaSenha; // o hook pre-save fará o hash
await usuario.save();
res.json({ mensagem: "Senha alterada com sucesso." });
} catch (erro) {
next(erro);
}
}
module.exports = { registrar, login, eu, alterarSenha };
// src/controllers/tarefaController.js
const Tarefa = require("../models/Tarefa");
const mongoose = require("mongoose");
// GET /tarefas
async function listar(req, res, next) {
try {
const {
status,
prioridade,
busca,
pagina = 1,
por_pagina = 10,
ordenar = "-createdAt",
} = req.query;
// Filtro base — apenas tarefas do usuário logado
const filtros = { usuario: req.usuario._id };
if (status) filtros.status = status;
if (prioridade) filtros.prioridade = prioridade;
if (busca) filtros.titulo = { $regex: busca, $options: "i" };
const [tarefas, total] = await Promise.all([
Tarefa.find(filtros)
.sort(ordenar)
.skip((pagina - 1) * por_pagina)
.limit(Number(por_pagina))
.lean(),
Tarefa.countDocuments(filtros),
]);
res.json({
dados: tarefas,
paginacao: {
total,
pagina: Number(pagina),
por_pagina: Number(por_pagina),
total_paginas: Math.ceil(total / por_pagina),
},
});
} catch (erro) {
next(erro);
}
}
// GET /tarefas/estatisticas
async function estatisticas(req, res, next) {
try {
const stats = await Tarefa.aggregate([
{ $match: { usuario: req.usuario._id } },
{
$group: {
_id: "$status",
total: { $sum: 1 },
comPrazo: {
$sum: { $cond: [{ $ne: ["$prazo", null] }, 1, 0] },
},
},
},
{
$project: {
status: "$_id",
total: 1,
comPrazo: 1,
_id: 0,
},
},
]);
const atrasadas = await Tarefa.countDocuments({
usuario: req.usuario._id,
prazo: { $lt: new Date() },
status: { $nin: ["concluida", "cancelada"] },
});
res.json({ porStatus: stats, atrasadas });
} catch (erro) {
next(erro);
}
}
// GET /tarefas/:id
async function buscarUma(req, res, next) {
try {
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ erro: "ID inválido." });
}
const tarefa = await Tarefa.findOne({
_id: req.params.id,
usuario: req.usuario._id, // garante que é do usuário logado
});
if (!tarefa) {
return res.status(404).json({ erro: "Tarefa não encontrada." });
}
res.json(tarefa);
} catch (erro) {
next(erro);
}
}
// POST /tarefas
async function criar(req, res, next) {
try {
const { titulo, descricao, status, prioridade, prazo, tags } = req.body;
const tarefa = await Tarefa.create({
titulo,
descricao,
status,
prioridade,
prazo: prazo ? new Date(prazo) : null,
tags,
usuario: req.usuario._id,
});
res.status(201).json(tarefa);
} catch (erro) {
next(erro);
}
}
// PUT /tarefas/:id
async function atualizar(req, res, next) {
try {
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ erro: "ID inválido." });
}
const { titulo, descricao, status, prioridade, prazo, tags } = req.body;
const tarefa = await Tarefa.findOneAndUpdate(
{ _id: req.params.id, usuario: req.usuario._id },
{
$set: {
...(titulo !== undefined && { titulo }),
...(descricao !== undefined && { descricao }),
...(status !== undefined && { status }),
...(prioridade !== undefined && { prioridade }),
...(tags !== undefined && { tags }),
prazo: prazo ? new Date(prazo) : null,
},
},
{ new: true, runValidators: true }
);
if (!tarefa) {
return res.status(404).json({ erro: "Tarefa não encontrada." });
}
res.json(tarefa);
} catch (erro) {
next(erro);
}
}
// DELETE /tarefas/:id
async function remover(req, res, next) {
try {
if (!mongoose.isValidObjectId(req.params.id)) {
return res.status(400).json({ erro: "ID inválido." });
}
const tarefa = await Tarefa.findOneAndDelete({
_id: req.params.id,
usuario: req.usuario._id,
});
if (!tarefa) {
return res.status(404).json({ erro: "Tarefa não encontrada." });
}
res.json({ mensagem: `Tarefa "${tarefa.titulo}" removida com sucesso.` });
} catch (erro) {
next(erro);
}
}
module.exports = { listar, estatisticas, buscarUma, criar, atualizar, remover };
Rotas
// src/routes/auth.js
const express = require("express");
const router = express.Router();
const { registrar, login, eu, alterarSenha } = require("../controllers/authController");
const { autenticar } = require("../middlewares/auth");
router.post("/registrar", registrar);
router.post("/login", login);
router.get("/eu", autenticar, eu);
router.patch("/senha", autenticar, alterarSenha);
module.exports = router;
// src/routes/tarefas.js
const express = require("express");
const router = express.Router();
const ctrl = require("../controllers/tarefaController");
const { autenticar } = require("../middlewares/auth");
// Todas as rotas de tarefas exigem autenticação
router.use(autenticar);
router.get("/", ctrl.listar);
router.get("/estatisticas", ctrl.estatisticas);
router.get("/:id", ctrl.buscarUma);
router.post("/", ctrl.criar);
router.put("/:id", ctrl.atualizar);
router.delete("/:id", ctrl.remover);
module.exports = router;
Middlewares de suporte
// src/middlewares/logger.js
function logger(req, res, next) {
const inicio = Date.now();
res.on("finish", () => {
const ms = Date.now() - inicio;
const ok = res.statusCode < 400;
const cor = ok ? "x1b[32m" : "x1b[31m";
const reset = "x1b[0m";
console.log(
`${cor}[${new Date().toLocaleTimeString("pt-BR")}]${reset} ` +
`${req.method.padEnd(7)} ${req.path.padEnd(30)} ` +
`${cor}${res.statusCode}${reset} ${ms}ms`
);
});
next();
}
module.exports = logger;
// src/middlewares/erros.js
function naoEncontrado(req, res) {
res.status(404).json({
erro: `Rota ${req.method} ${req.path} não encontrada.`,
});
}
function tratadorDeErros(erro, req, res, next) {
console.error(`[Erro] ${erro.name}: ${erro.message}`);
if (erro.name === "CastError") {
return res.status(400).json({ erro: `ID inválido: ${erro.value}` });
}
if (erro.name === "ValidationError") {
const detalhes = Object.values(erro.errors).map(e => e.message);
return res.status(422).json({ erro: "Dados inválidos.", detalhes });
}
if (erro.code === 11000) {
const campo = Object.keys(erro.keyValue)[0];
return res.status(409).json({ erro: `${campo} já está em uso.` });
}
if (erro.type === "entity.parse.failed") {
return res.status(400).json({ erro: "JSON inválido no corpo da requisição." });
}
const status = erro.status || 500;
res.status(status).json({
erro: status === 500 ? "Erro interno do servidor." : erro.message,
});
}
module.exports = { naoEncontrado, tratadorDeErros };
O ponto de entrada
// src/config/database.js
const mongoose = require("mongoose");
async function conectar() {
await mongoose.connect(process.env.MONGODB_URL);
console.log(`✅ MongoDB: ${mongoose.connection.host}`);
mongoose.connection.on("error", err =>
console.error("❌ MongoDB erro:", err.message)
);
}
module.exports = { conectar };
// src/index.js
require("dotenv").config();
const express = require("express");
const { conectar } = require("./config/database");
const logger = require("./middlewares/logger");
const { naoEncontrado, tratadorDeErros } = require("./middlewares/erros");
const authRoutes = require("./routes/auth");
const tarefasRoutes = require("./routes/tarefas");
const app = express();
// ── Middlewares globais ────────────────────────────
app.use(logger);
app.use(express.json());
// ── CORS ──────────────────────────────────────────
app.use((req, res, next) => {
res.set({
"Access-Control-Allow-Origin": "*",
"Access-Control-Allow-Methods": "GET, POST, PUT, PATCH, DELETE, OPTIONS",
"Access-Control-Allow-Headers": "Content-Type, Authorization",
});
if (req.method === "OPTIONS") return res.sendStatus(204);
next();
});
// ── Rota de health check ──────────────────────────
app.get("/health", (req, res) => {
res.json({
status: "ok",
timestamp: new Date().toISOString(),
uptime: Math.floor(process.uptime()) + "s",
});
});
// ── Rotas da API ──────────────────────────────────
app.use("/auth", authRoutes);
app.use("/tarefas", tarefasRoutes);
// ── Erros ─────────────────────────────────────────
app.use(naoEncontrado);
app.use(tratadorDeErros);
// ── Inicialização ─────────────────────────────────
const PORTA = process.env.PORT || 3000;
conectar()
.then(() => {
app.listen(PORTA, () => {
console.log(`
🚀 API rodando em http://localhost:${PORTA}`);
console.log(`📋 Ambiente: ${process.env.NODE_ENV || "development"}
`);
});
})
.catch(err => {
console.error("Falha na inicialização:", err.message);
process.exit(1);
});
// Encerramento gracioso
process.on("SIGTERM", () => {
console.log("Encerrando servidor...");
process.exit(0);
});
module.exports = app;
Testando a API completa
# 1. Registrar usuário
curl -X POST http://localhost:3000/auth/registrar
-H "Content-Type: application/json"
-d '{"nome": "Ana Paula", "email": "ana@email.com", "senha": "senha123"}'
# 2. Login — guarde o token retornado
curl -X POST http://localhost:3000/auth/login
-H "Content-Type: application/json"
-d '{"email": "ana@email.com", "senha": "senha123"}'
# 3. Criar tarefa (use o token do login)
curl -X POST http://localhost:3000/tarefas
-H "Content-Type: application/json"
-H "Authorization: Bearer SEU_TOKEN_AQUI"
-d '{"titulo": "Estudar Node.js", "prioridade": "alta", "prazo": "2025-12-31"}'
# 4. Listar tarefas
curl http://localhost:3000/tarefas
-H "Authorization: Bearer SEU_TOKEN_AQUI"
# 5. Filtrar por status
curl "http://localhost:3000/tarefas?status=pendente&pagina=1&por_pagina=5"
-H "Authorization: Bearer SEU_TOKEN_AQUI"
# 6. Estatísticas
curl http://localhost:3000/tarefas/estatisticas
-H "Authorization: Bearer SEU_TOKEN_AQUI"
# 7. Sem token — deve retornar 401
curl http://localhost:3000/tarefas
O que este projeto exercitou
| Conceito do Módulo 4 | Onde foi aplicado |
|---|---|
| Node.js e process | dotenv, process.exit, SIGTERM |
| NPM e package.json | scripts, dependencies, devDependencies |
| Servidor HTTP / Express | app, router, middlewares |
| Router modular | routes/auth.js, routes/tarefas.js |
| Middlewares customizados | logger, autenticar, erros |
| MongoDB e Mongoose | Models, CRUD, aggregate |
| Schema com validações | Usuario.js, Tarefa.js |
| Hooks do Mongoose | pre-save para hash de senha |
| Virtuals | tarefa.atrasada |
| Autenticação JWT | registro, login, token, rotas protegidas |
| Hash de senhas | bcrypt com salt 12 |
| Tratamento de erros | ValidationError, CastError, 11000 |
| Paginação | skip, limit, countDocuments |
Conclusão do Módulo 4
Você construiu uma API REST profissional do zero — com autenticação, banco de dados, paginação, filtros e tratamento robusto de erros. Esse é o tipo de projeto que vai direto para o portfólio.
O Módulo 5 começa no próximo artigo com testes automatizados — uma habilidade que separa desenvolvedores júnior de sênior e que todo projeto profissional exige.
📚 Fontes e Referências
- JSON Web Tokens — jwt.io: https://jwt.io
- jsonwebtoken — Docs: https://github.com/auth0/node-jsonwebtoken
- bcrypt — Docs: https://github.com/kelektiv/node.bcrypt.js
- OWASP — Authentication Cheat Sheet: https://cheatsheetseries.owasp.org/cheatsheets/Authentication_Cheat_Sheet.html
- Express.js — Documentação: https://expressjs.com/pt-br
- Mongoose — Documentação: https://mongoosejs.com/docs
- MongoDB Atlas: https://www.mongodb.com/atlas
- Node.js Design Patterns — Mario Casciaro (Packt Publishing)
- roadmap.sh — Backend roadmap: https://roadmap.sh/backend