Crie um gerenciador de tarefas simples com categorias usando Node.js e MongoDB
Aprenda a organizar tarefas por categoria usando Node.js e MongoDB. Construa uma API REST com endpoints de criar, listar, obter, atualizar, excluir e filtro por categoria.
Uma lista de tarefas simples funciona bem no começo: você adiciona algumas tarefas e pode marcá-las como concluídas quando terminar. Mas, conforme o volume aumenta, ela fica difícil de usar. Uma solução é adicionar categorias, permitindo que cada tarefa pertença a grupos como Trabalho, Estudos, Pessoal etc., e você possa filtrar a lista pela categoria desejada.
Neste tutorial, você vai aprender a criar uma API REST que organiza tarefas por categoria usando Node.js e MongoDB. Ao final, você terá um servidor Node.js funcional com cinco endpoints: criar, listar, obter, atualizar e excluir, além de um filtro por categoria. Você também verá como aplicar validação de categorias usando o driver nativo do MongoDB, sem depender de um ODM como o Mongoose.
O que você vai aprender
Como criar uma API REST com Node.js, Express e MongoDB, incluindo endpoints completos de CRUD
Como validar entradas, aplicar categorias e lidar com valores de ObjectId de forma segura
Como estruturar e testar sua API e depois conectá-la a um frontend
Você encontra o código completo deste tutorial no GitHub, caso prefira clonar o projeto e acompanhar a leitura.
Pré-requisitos
Antes de começar, você deve ter:
Node.js 18+ instalado (npm é incluído por padrão)
MongoDB rodando localmente ou um cluster gratuito no MongoDB Atlas
Postman ou qualquer cliente HTTP
Conhecimentos básicos de JavaScript e conceitos de REST
Passo 1: configure o projeto
Vamos construir nosso gerenciador de tarefas simples. Primeiro, crie uma nova pasta e inicialize um projeto Node executando os comandos abaixo no terminal:
mkdir task-manager-categories
cd task-manager-categories
npm init -yTecnologia
Em seguida, vamos instalar quatro dependências de runtime necessárias: express para rotas, o driver oficial do MongoDB, dotenv para carregar variáveis de ambiente e, por fim, helmet para cabeçalhos de segurança.
Para desenvolvimento, instale o nodemon como dependência de dev usando o comando abaixo, para o servidor recarregar automaticamente quando houver mudanças nos arquivos:
Depois, abra a pasta do projeto na sua IDE e atualize a seção scripts do package.json para incluir o seguinte. Assim, você pode iniciar o servidor com npm start ou rodá-lo no modo de desenvolvimento com npm run dev:
Aqui está a estrutura que vamos construir. Cada pasta tem um papel claro: db/ cuida da conexão com o banco, lib/ contém funções utilitárias, middleware/ guarda os middlewares do Express e routes/ define os handlers das requisições. A pasta public/ vai armazenar um pequeno frontend que adicionaremos depois. Mesmo em um projeto pequeno, essa estrutura facilita o entendimento e deixa claro onde cada parte do código deve ficar. Fique à vontade para criar a estrutura de pastas agora e ir preenchendo aos poucos, ou pular este passo e adicionar os arquivos conforme avançamos:
Para armazenar e buscar tarefas, primeiro precisamos conectar nosso app ao MongoDB. Isso é feito com uma string de conexão, que informa ao driver do MongoDB como acessar seu banco. Em vez de codificá-la direto no código, é melhor mantê-la em um arquivo de ambiente. Isso mantém valores sensíveis fora do repositório e facilita alternar entre ambientes.
Crie um arquivo .env.example para documentar as variáveis necessárias e um arquivo .env com seus valores reais, assim:
# Copie este arquivo para `.env` e preencha com valores reais.# --- MongoDB local ---# Use isto se você estiver rodando o MongoDB localmenteMONGO_URI=mongodb://127.0.0.1:27017
# --- MongoDB Atlas ---# Substitua <username>, <password> e <cluster-url> pelos seus valores# Exemplo: mongodb+srv://user:[email protected]/# MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majorityDB_NAME=taskmanager
PORT=3000Tecnologia
Com a string de conexão definida, podemos usá-la para configurar a conexão com o MongoDB. Crie o arquivo db/connect.js. É aqui que vamos inicializar o cliente do MongoDB e disponibilizá-lo para o resto do app:
const{ MongoClient }=require("mongodb");let client;let db;asyncfunctionconnectDB(){if(!process.env.MONGO_URI){thrownewError("MONGO_URI is not set. Check your .env file.");}
client =newMongoClient(process.env.MONGO_URI,{appName:"devrel-tutorial-javascript-crud-geeksforgeeks",});await client.connect();
db = client.db(process.env.DB_NAME||"taskmanager");await db.collection("tasks").createIndex({category:1});
console.log(`MongoDB connected (db: ${db.databaseName})`);}functiongetTasksCollection(){if(!db){thrownewError("Database not initialized. Call connectDB() first.");}return db.collection("tasks");}asyncfunctioncloseDB(){if(client)await client.close();}
module.exports ={ connectDB, getTasksCollection, closeDB };Tecnologia
Este arquivo configura um único cliente do MongoDB reutilizado pelo app inteiro. Você só deve criá-lo uma vez, já que o driver faz o pool de conexões automaticamente. Criar um novo cliente a cada requisição pode parecer ok no começo, mas causa gargalos de performance rapidamente.
Passo 3: defina a estrutura da tarefa e a validação
Agora que o app se conecta ao MongoDB, precisamos definir como é uma tarefa antes de salvar qualquer coisa.
Aqui, usar o driver nativo do MongoDB é um pouco diferente. Não há um arquivo de schema como no Mongoose. Em vez disso, o "schema" é o formato do objeto que você insere no banco. Pode parecer flexível demais no início, mas é útil porque você fica mais próximo do funcionamento real do MongoDB, sem camadas de abstração escondendo a lógica.
Ainda assim, queremos validação. Em vez de espalhar essa lógica pelas rotas, vamos centralizá-la em um único lugar para que tudo siga as mesmas regras. Crie o arquivo lib/taskDocument.js e adicione o código abaixo:
classValidationErrorextendsError{constructor(message){super(message);this.name ='ValidationError';}}constALLOWED_CATEGORIES=['Work','Personal','Study','Other'];functionassertValidCategory(category){if(!ALLOWED_CATEGORIES.includes(category)){thrownewValidationError(`category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`);}}functionbuildTaskDocument(body ={}){if(!body.title ||typeof body.title !=='string'||!body.title.trim()){thrownewValidationError('title is required and must be a non-empty string');}if(body.category !=null){assertValidCategory(body.category);}const now =newDate();return{title: body.title.trim(),description:typeof body.description ==='string'? body.description.trim():'',category: body.category !=null? body.category :'Other',completed:Boolean(body.completed),createdAt: now,updatedAt: now
};}functionbuildTaskUpdate(body ={}){const updates ={};if(typeof body.title ==='string'&& body.title.trim()){
updates.title = body.title.trim();}if(typeof body.description ==='string'){
updates.description = body.description.trim();}if(body.category !=null){assertValidCategory(body.category);
updates.category = body.category;}if(body.completed !=null){
updates.completed =Boolean(body.completed);}if(Object.keys(updates).length ===0){thrownewValidationError('no valid fields provided for update');}
updates.updatedAt =newDate();return updates;}
module.exports ={ALLOWED_CATEGORIES,
buildTaskDocument,
buildTaskUpdate,
ValidationError
};Tecnologia
Este arquivo funciona como porteiro do que entra no seu banco. Toda criação ou atualização passa por aqui, então as regras são aplicadas em um único lugar.
Ele garante que as tarefas tenham o formato certo, mantém as categorias consistentes e evita surpresas quando você começar a consultar os dados. A ValidationError personalizada também separa entradas inválidas de problemas reais do servidor, permitindo respostas adequadas da API. Com isso pronto, o resto do app fica simples: cada rota foca no seu papel, sabendo que os dados já chegam validados. No próximo passo, vamos conectar as rotas e começar a salvar tarefas no MongoDB.
Passo 4: trate ObjectId e validação da requisição
Agora que sabemos como é uma tarefa, vamos definir como referenciá-la. Sempre que uma rota recebe um parâmetro :id, ele chega como string comum. Mas o MongoDB espera um ObjectId. Se a string for inválida, o driver lança um erro pouco amigável. Em vez de tratar isso em cada rota, vamos centralizar com um middleware para que todos os endpoints se comportem de forma consistente.
Para isso, crie o arquivo middleware/parseObjectId.js e adicione o seguinte:
Este middleware roda antes do handler da rota. Ele valida o id, converte-o para ObjectId e anexa em req.taskId. Assim, quando a lógica da rota executar, você já está trabalhando com um ObjectId correto e entradas inválidas são rejeitadas cedo com uma resposta 400. No próximo passo, vamos plugá-lo nas rotas e ligar tudo.
Passo 5: crie as rotas de tarefas (CRUD + filtro)
A essa altura, o mais difícil já foi. Definimos o que é uma tarefa válida e como IDs são analisados e validados. Isso permite que os handlers de rota foquem em uma coisa: conversar com o banco.
Vamos criar as rotas da API no arquivo routes/tasks.js e adicionar o seguinte conteúdo:
const express =require('express');const{ getTasksCollection }=require('../db/connect');const{ALLOWED_CATEGORIES,
buildTaskDocument,
buildTaskUpdate,
ValidationError
}=require('../lib/taskDocument');const parseObjectId =require('../middleware/parseObjectId');const router = express.Router();// POST /tasks
router.post('/',async(req, res, next)=>{try{const doc =buildTaskDocument(req.body);const result =awaitgetTasksCollection().insertOne(doc);
res.status(201).json({_id: result.insertedId,...doc });}catch(err){if(err instanceofValidationError){return res.status(400).json({error: err.message });}next(err);}});// GET /tasks (optionally ?category=Work)
router.get('/',async(req, res, next)=>{try{const{ category }= req.query;if(category &&!ALLOWED_CATEGORIES.includes(category)){return res.status(400).json({error:`category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`});}const filter = category ?{ category }:{};const tasks =awaitgetTasksCollection().find(filter).sort({createdAt:-1}).toArray();
res.json(tasks);}catch(err){next(err);}});// GET /tasks/:id
router.get('/:id', parseObjectId,async(req, res, next)=>{try{const task =awaitgetTasksCollection().findOne({_id: req.taskId });if(!task)return res.status(404).json({error:'task not found'});
res.json(task);}catch(err){next(err);}});// PUT /tasks/:id
router.put('/:id', parseObjectId,async(req, res, next)=>{try{const updates =buildTaskUpdate(req.body);const result =awaitgetTasksCollection().findOneAndUpdate({_id: req.taskId },{$set: updates },{returnDocument:'after'});if(!result)return res.status(404).json({error:'task not found'});
res.json(result);}catch(err){if(err instanceofValidationError){return res.status(400).json({error: err.message });}next(err);}});// DELETE /tasks/:id
router.delete('/:id', parseObjectId,async(req, res, next)=>{try{const result =awaitgetTasksCollection().deleteOne({_id: req.taskId });if(result.deletedCount ===0){return res.status(404).json({error:'task not found'});}
res.status(204).end();}catch(err){next(err);}});
module.exports = router;Tecnologia
Cada rota segue o mesmo padrão: recebe a entrada, passa pelos helpers que criamos, chama o MongoDB e retorna a resposta. Com a validação e o parsing de ID já resolvidos, o código permanece enxuto e previsível. Na prática, funciona assim:
A rota POST monta uma nova tarefa com buildTaskDocument antes de inserir
As rotas GET podem filtrar por categoria e retornam resultados do mais novo para o mais antigo
A rota PUT usa o buildTaskUpdate, então atualizações parciais são seguras e consistentes
A rota DELETE remove a tarefa pelo ObjectId já validado
Alguns detalhes do código que valem destaque:
find() retorna um cursor, não um array; por isso encadeamos .toArray() após o sort para obter os resultados
findOneAndUpdate com returnDocument: 'after' já retorna o documento atualizado
Retornamos códigos HTTP adequados: 201 para criação, 204 para exclusão e 400 ou 404 quando apropriado
Erros inesperados são encaminhados para next(err), para tratamento centralizado, sem repetir lógica em cada rota
Passo 6: conecte tudo no servidor
Agora, temos todas as peças: validação, rotas limpas e conexão com o banco. Falta apenas ligar tudo e iniciar o servidor. Crie o arquivo server.js, ponto de entrada do app, onde tudo se encontra:
require('dotenv').config();const path =require('path');const express =require('express');const helmet =require('helmet');const{ connectDB, closeDB }=require('./db/connect');const tasksRouter =require('./routes/tasks');if(!process.env.MONGO_URI){
console.error('Fatal: MONGO_URI is not set. Copy .env.example to .env and fill it in.');
process.exit(1);}constPORT=Number(process.env.PORT)||3000;const app =express();
app.use(helmet());
app.use(express.json({limit:'100kb'}));
app.use(express.static(path.join(__dirname,'public')));
app.get('/health',(req, res)=>{
res.json({status:'ok',service:'task-manager-with-categories'});});
app.use('/tasks', tasksRouter);
app.use((req, res)=>{
res.status(404).json({error:'not found'});});
app.use((err, req, res, next)=>{if(err.type ==='entity.too.large'){return res.status(413).json({error:'payload too large'});}if(err.type ==='entity.parse.failed'){return res.status(400).json({error:'invalid JSON body'});}
console.error(err);
res.status(500).json({error:'internal server error'});});asyncfunctionstart(){try{awaitconnectDB();const server = app.listen(PORT,()=>{
console.log(`Server running on http://localhost:${PORT}`);});constshutdown=()=>{
console.log('\nShutting down gracefully...');
server.close(async()=>{awaitcloseDB();
process.exit(0);});setTimeout(()=> process.exit(1),10_000).unref();};
process.on('SIGINT', shutdown);
process.on('SIGTERM', shutdown);}catch(err){
console.error('Failed to start server:', err);
process.exit(1);}}if(require.main === module){start();}
module.exports ={ app, start };Tecnologia
Este arquivo amarra tudo de forma limpa. Ele verifica as variáveis de ambiente antes de rodar, aplica segurança básica com helmet e limita o tamanho do JSON para evitar cargas acidentais muito grandes. Também serve a pasta public/, permitindo que o frontend rode no mesmo app sem precisar de outro servidor.
Todas as rotas ficam sob /tasks e qualquer caminho não encontrado retorna um 404 claro. Os erros são tratados em um único lugar, o que mantém consistência e facilita a manutenção. A conexão com o banco é estabelecida antes do servidor começar a ouvir, então você não aceita requisições antes de estar pronto, e o desligamento é gracioso, fechando conexões corretamente.
Agora vamos subir tudo e conferir se está funcionando com npm run dev. Se estiver tudo certo, você verá algo assim:
MongoDB connected (db: taskmanager)
Server running on http://localhost:3000
Tecnologia
Pronto! Tudo ligado e rodando. Agora temos uma API funcional, então vamos testá-la.
Passo 7: teste os endpoints da API
Antes de construir uma UI, vale conferir se cada endpoint funciona sozinho. Isso facilita o debug, já que você identifica rapidamente se o problema vem da API ou do frontend. Vamos usar o Postman (ou qualquer cliente HTTP) e executar algumas requisições em ordem. Cada uma aproveita os dados criados na etapa anterior.
Crie uma tarefa: envie uma requisição POST com title, description e category. Se tudo der certo, a API retorna 201 Created com a nova tarefa e o _id gerado.
POST http://localhost:3000/tasks
Content-Type: application/json
{"title":"Write GeeksForGeeks article",
"description":"First draft by Friday",
"category":"Work"}Tecnologia
Crie mais algumas: repita a requisição POST com corpos diferentes para ter dados suficientes para testar listagem e filtros. Assim você terá um pequeno conjunto de dados para consultas.
{"title":"Go for a run","category":"Personal"}{"title":"Read MongoDB docs","category":"Study"}{"title":"Buy groceries"}// category defaults to "Other"Tecnologia
Liste todas as tarefas: retorna todas as tarefas, do mais novo para o mais antigo.
Filtre por categoria: retorna apenas as tarefas de Work. O índice criado antes mantém essa consulta rápida conforme os dados crescem.
GET http://localhost:3000/tasks?category=WorkTecnologia
Atualize uma tarefa: use o _id de uma das tarefas criadas (copie da resposta do POST ou do endpoint de listagem). A resposta traz a tarefa atualizada. Envie apenas os campos que deseja alterar para manter as atualizações parciais simples.
PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json
{"completed":true}Tecnologia
Exclua uma tarefa: use o _id da tarefa que deseja remover. A resposta é 204 No Content. Se tentar buscar a mesma tarefa depois, receberá 404 com "task not found".
Pronto, você confirmou que todas as rotas se comportam como esperado. A API está cumprindo seu papel, então vamos criar uma interface por cima dela.
Passo 8: adicione um frontend para interagir com a API
O Postman é ótimo para validar a API, mas é melhor ainda ver tudo funcionando em um site. Vamos adicionar um pouco de frontend ao nosso app.
Em vez de colar HTML e CSS extensos aqui, subi o código neste repositório do GitHub. Vá até lá e copie o conteúdo de index.html, styles.css e favicon.svg para a pasta public/ na raiz do seu projeto. Salve as mudanças e acesse localhost:3000. Depois cole o código abaixo no arquivo app.js:
O JavaScript é onde tudo se reconecta à API que você construiu. Cada função mapeia para uma rota, deixando o frontend fácil de acompanhar. Todas as requisições passam por um único helper api(), então você trata erros em um só lugar, sem repetir lógica.
O filtro funciona passando uma query string como ?category=Work, diretamente integrado à lógica do backend. Após criar, atualizar ou excluir uma tarefa, o app busca a lista mais recente para manter a UI sincronizada. O contador de tarefas é um detalhe simples, mas ajuda o app a parecer mais vivo durante o uso.
Agora reinicie o servidor com npm run dev, depois abra http://localhost:3000 para testar o Task Manager. Adicione algumas tarefas, atualize, alterne entre categorias e exclua também. Agora o frontend e o backend estão totalmente conectados, de ponta a ponta.
Resumo
Parabéns! Você construiu do zero uma API completa de gerenciador de tarefas com Node.js, Express e MongoDB. Você aplicou validação, manteve as rotas enxutas e conectou tudo em um sistema funcional. Mais importante: ficou próximo do funcionamento real do MongoDB, sem esconder nada atrás de abstrações. A partir daqui, você pode evoluir o app para deixá-lo pronto para produção.
Principais aprendizados
Usar middleware e centralizar a validação mantém o código limpo e previsível.
O driver nativo do MongoDB dá controle sem abstrações desnecessárias.
Uma API bem estruturada é fácil de evoluir para um app pronto para produção.
FAQs
Preciso do Mongoose para criar uma API com MongoDB?⌄
Não. Este tutorial usa o driver nativo do MongoDB, que dá mais controle e mantém tudo leve. Você pode adicionar Mongoose depois, se quiser abstrações de schema.
Como valido dados sem um schema?⌄
Você pode centralizar a validação em funções utilitárias (como buildTaskDocument), garantindo que toda rota aplique as mesmas regras antes de gravar no banco.
O que acontece se eu passar um ObjectId inválido?⌄
O middleware rejeita cedo com uma resposta 400, evitando que o MongoDB dispare erros confusos.
Aumente o nível de suas habilidades em Python com estes cinco desafios de codificação em Python. Faça um teste para ver se você consegue completar um em uma semana!
Este guia completo ensina os fundamentos do desenvolvimento backend com Python. Aprenda conceitos básicos, frameworks e boas práticas para começar a criar aplicações web.
Saiba como usar os acionadores SQL para automatizar tarefas, manter a integridade dos dados e melhorar o desempenho do banco de dados. Experimente exemplos práticos como os comandos CREATE, ALTER e DROP no MySQL e no Oracle.
Uma lista de tarefas simples funciona bem no começo: você adiciona algumas tarefas e pode marcá-las como concluídas quando terminar. Mas, conforme o volume aumenta, ela fica difícil de usar. Uma solução é adicionar categorias, permitindo que cada tarefa pertença a grupos como Trabalho, Estudos, Pessoal etc., e você possa filtrar a lista pela categoria desejada.
Neste tutorial, você vai aprender a criar uma API REST que organiza tarefas por categoria usando Node.js e MongoDB. Ao final, você terá um servidor Node.js funcional com cinco endpoints: criar, listar, obter, atualizar e excluir, além de um filtro por categoria. Você também verá como aplicar validação de categorias usando o driver nativo do MongoDB, sem depender de um ODM como o Mongoose.
O que você vai aprender
Você encontra o código completo deste tutorial no GitHub, caso prefira clonar o projeto e acompanhar a leitura.
Pré-requisitos
Antes de começar, você deve ter:
Passo 1: configure o projeto
Vamos construir nosso gerenciador de tarefas simples. Primeiro, crie uma nova pasta e inicialize um projeto Node executando os comandos abaixo no terminal:
Em seguida, vamos instalar quatro dependências de runtime necessárias: express para rotas, o driver oficial do MongoDB, dotenv para carregar variáveis de ambiente e, por fim, helmet para cabeçalhos de segurança.
Para desenvolvimento, instale o nodemon como dependência de dev usando o comando abaixo, para o servidor recarregar automaticamente quando houver mudanças nos arquivos:
Depois, abra a pasta do projeto na sua IDE e atualize a seção scripts do package.json para incluir o seguinte. Assim, você pode iniciar o servidor com npm start ou rodá-lo no modo de desenvolvimento com npm run dev:
Aqui está a estrutura que vamos construir. Cada pasta tem um papel claro: db/ cuida da conexão com o banco, lib/ contém funções utilitárias, middleware/ guarda os middlewares do Express e routes/ define os handlers das requisições. A pasta public/ vai armazenar um pequeno frontend que adicionaremos depois. Mesmo em um projeto pequeno, essa estrutura facilita o entendimento e deixa claro onde cada parte do código deve ficar. Fique à vontade para criar a estrutura de pastas agora e ir preenchendo aos poucos, ou pular este passo e adicionar os arquivos conforme avançamos:
Passo 2: conecte ao MongoDB
Para armazenar e buscar tarefas, primeiro precisamos conectar nosso app ao MongoDB. Isso é feito com uma string de conexão, que informa ao driver do MongoDB como acessar seu banco. Em vez de codificá-la direto no código, é melhor mantê-la em um arquivo de ambiente. Isso mantém valores sensíveis fora do repositório e facilita alternar entre ambientes.
Crie um arquivo .env.example para documentar as variáveis necessárias e um arquivo .env com seus valores reais, assim:
Com a string de conexão definida, podemos usá-la para configurar a conexão com o MongoDB. Crie o arquivo db/connect.js. É aqui que vamos inicializar o cliente do MongoDB e disponibilizá-lo para o resto do app:
Este arquivo configura um único cliente do MongoDB reutilizado pelo app inteiro. Você só deve criá-lo uma vez, já que o driver faz o pool de conexões automaticamente. Criar um novo cliente a cada requisição pode parecer ok no começo, mas causa gargalos de performance rapidamente.
Passo 3: defina a estrutura da tarefa e a validação
Agora que o app se conecta ao MongoDB, precisamos definir como é uma tarefa antes de salvar qualquer coisa.
Aqui, usar o driver nativo do MongoDB é um pouco diferente. Não há um arquivo de schema como no Mongoose. Em vez disso, o "schema" é o formato do objeto que você insere no banco. Pode parecer flexível demais no início, mas é útil porque você fica mais próximo do funcionamento real do MongoDB, sem camadas de abstração escondendo a lógica.
Ainda assim, queremos validação. Em vez de espalhar essa lógica pelas rotas, vamos centralizá-la em um único lugar para que tudo siga as mesmas regras. Crie o arquivo lib/taskDocument.js e adicione o código abaixo:
Este arquivo funciona como porteiro do que entra no seu banco. Toda criação ou atualização passa por aqui, então as regras são aplicadas em um único lugar.
Ele garante que as tarefas tenham o formato certo, mantém as categorias consistentes e evita surpresas quando você começar a consultar os dados. A ValidationError personalizada também separa entradas inválidas de problemas reais do servidor, permitindo respostas adequadas da API. Com isso pronto, o resto do app fica simples: cada rota foca no seu papel, sabendo que os dados já chegam validados. No próximo passo, vamos conectar as rotas e começar a salvar tarefas no MongoDB.
Passo 4: trate ObjectId e validação da requisição
Agora que sabemos como é uma tarefa, vamos definir como referenciá-la. Sempre que uma rota recebe um parâmetro :id, ele chega como string comum. Mas o MongoDB espera um ObjectId. Se a string for inválida, o driver lança um erro pouco amigável. Em vez de tratar isso em cada rota, vamos centralizar com um middleware para que todos os endpoints se comportem de forma consistente.
Para isso, crie o arquivo middleware/parseObjectId.js e adicione o seguinte:
Este middleware roda antes do handler da rota. Ele valida o id, converte-o para ObjectId e anexa em req.taskId. Assim, quando a lógica da rota executar, você já está trabalhando com um ObjectId correto e entradas inválidas são rejeitadas cedo com uma resposta 400. No próximo passo, vamos plugá-lo nas rotas e ligar tudo.
Passo 5: crie as rotas de tarefas (CRUD + filtro)
A essa altura, o mais difícil já foi. Definimos o que é uma tarefa válida e como IDs são analisados e validados. Isso permite que os handlers de rota foquem em uma coisa: conversar com o banco.
Vamos criar as rotas da API no arquivo routes/tasks.js e adicionar o seguinte conteúdo:
Cada rota segue o mesmo padrão: recebe a entrada, passa pelos helpers que criamos, chama o MongoDB e retorna a resposta. Com a validação e o parsing de ID já resolvidos, o código permanece enxuto e previsível. Na prática, funciona assim:
Alguns detalhes do código que valem destaque:
Passo 6: conecte tudo no servidor
Agora, temos todas as peças: validação, rotas limpas e conexão com o banco. Falta apenas ligar tudo e iniciar o servidor. Crie o arquivo server.js, ponto de entrada do app, onde tudo se encontra:
Este arquivo amarra tudo de forma limpa. Ele verifica as variáveis de ambiente antes de rodar, aplica segurança básica com helmet e limita o tamanho do JSON para evitar cargas acidentais muito grandes. Também serve a pasta public/, permitindo que o frontend rode no mesmo app sem precisar de outro servidor.
Todas as rotas ficam sob /tasks e qualquer caminho não encontrado retorna um 404 claro. Os erros são tratados em um único lugar, o que mantém consistência e facilita a manutenção. A conexão com o banco é estabelecida antes do servidor começar a ouvir, então você não aceita requisições antes de estar pronto, e o desligamento é gracioso, fechando conexões corretamente.
Agora vamos subir tudo e conferir se está funcionando com npm run dev. Se estiver tudo certo, você verá algo assim:
Pronto! Tudo ligado e rodando. Agora temos uma API funcional, então vamos testá-la.
Passo 7: teste os endpoints da API
Antes de construir uma UI, vale conferir se cada endpoint funciona sozinho. Isso facilita o debug, já que você identifica rapidamente se o problema vem da API ou do frontend. Vamos usar o Postman (ou qualquer cliente HTTP) e executar algumas requisições em ordem. Cada uma aproveita os dados criados na etapa anterior.
Crie uma tarefa: envie uma requisição POST com title, description e category. Se tudo der certo, a API retorna 201 Created com a nova tarefa e o _id gerado.
Crie mais algumas: repita a requisição POST com corpos diferentes para ter dados suficientes para testar listagem e filtros. Assim você terá um pequeno conjunto de dados para consultas.
Liste todas as tarefas: retorna todas as tarefas, do mais novo para o mais antigo.
Filtre por categoria: retorna apenas as tarefas de Work. O índice criado antes mantém essa consulta rápida conforme os dados crescem.
Atualize uma tarefa: use o _id de uma das tarefas criadas (copie da resposta do POST ou do endpoint de listagem). A resposta traz a tarefa atualizada. Envie apenas os campos que deseja alterar para manter as atualizações parciais simples.
Exclua uma tarefa: use o _id da tarefa que deseja remover. A resposta é 204 No Content. Se tentar buscar a mesma tarefa depois, receberá 404 com "task not found".
Pronto, você confirmou que todas as rotas se comportam como esperado. A API está cumprindo seu papel, então vamos criar uma interface por cima dela.
Passo 8: adicione um frontend para interagir com a API
O Postman é ótimo para validar a API, mas é melhor ainda ver tudo funcionando em um site. Vamos adicionar um pouco de frontend ao nosso app.
Em vez de colar HTML e CSS extensos aqui, subi o código neste repositório do GitHub. Vá até lá e copie o conteúdo de index.html, styles.css e favicon.svg para a pasta public/ na raiz do seu projeto. Salve as mudanças e acesse localhost:3000. Depois cole o código abaixo no arquivo app.js:
O JavaScript é onde tudo se reconecta à API que você construiu. Cada função mapeia para uma rota, deixando o frontend fácil de acompanhar. Todas as requisições passam por um único helper api(), então você trata erros em um só lugar, sem repetir lógica.
O filtro funciona passando uma query string como ?category=Work, diretamente integrado à lógica do backend. Após criar, atualizar ou excluir uma tarefa, o app busca a lista mais recente para manter a UI sincronizada. O contador de tarefas é um detalhe simples, mas ajuda o app a parecer mais vivo durante o uso.
Agora reinicie o servidor com npm run dev, depois abra http://localhost:3000 para testar o Task Manager. Adicione algumas tarefas, atualize, alterne entre categorias e exclua também. Agora o frontend e o backend estão totalmente conectados, de ponta a ponta.
Resumo
Parabéns! Você construiu do zero uma API completa de gerenciador de tarefas com Node.js, Express e MongoDB. Você aplicou validação, manteve as rotas enxutas e conectou tudo em um sistema funcional. Mais importante: ficou próximo do funcionamento real do MongoDB, sem esconder nada atrás de abstrações. A partir daqui, você pode evoluir o app para deixá-lo pronto para produção.
Principais aprendizados