Javascript

Projeto: API REST com autenticação JWT Já leu

14 min de leitura

Projeto: API REST com autenticação JWT
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çã

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
Comentários

Mais em Javascript

Revisão  + Projeto Final: Produção Real
Revisão + Projeto Final: Produção Real

Chegamos ao fim de mais um módulo. Em três artigos cobrimos deploy, segurança...

Fetch API: consumindo dados da internet
Fetch API: consumindo dados da internet

At&eacute; agora simulamos requisi&ccedil;&otilde;es com&nbsp;setTimeout. A p...

Mini Projeto: Calculadora no Console
Mini Projeto: Calculadora no Console

Chegamos ao fim do primeiro m&oacute;dulo. Em nove artigos voc&ecirc; percorr...