Construiește un manager simplu de sarcini cu categorii folosind Node.js și MongoDB
Învață cum să-ți organizezi sarcinile pe categorii folosind Node.js și MongoDB. Construiește un API REST cu endpointuri de creare, listare, obținere, actualizare, ștergere și filtru după categorie.
O listă simplă de to-do merge perfect la început: adaugi câteva sarcini și le poți bifa după ce le termini. Dar pe măsură ce sarcinile se înmulțesc, devine greu de folosit. Un lucru care poate rezolva asta este adăugarea de categorii, astfel încât fiecare sarcină să aparțină unor categorii precum Work, Study, Personal etc., iar tu să poți filtra lista după categoria dorită.
În acest tutorial, vei învăța cum să construiești un API REST care îți permite să organizezi sarcinile pe categorii folosind Node.js și MongoDB. La final, vei avea un server Node.js funcțional cu cinci endpointuri: create, list, get, update și delete, plus un filtru după categorie. Vei vedea, de asemenea, cum să impui validarea categoriilor folosind driverul nativ MongoDB, fără să te bazezi pe un ODM precum Mongoose.
Ce vei învăța
Cum să construiești un API REST cu Node.js, Express și MongoDB, inclusiv endpointuri CRUD complete
Cum să validezi inputul, să impui categorii și să gestionezi în siguranță valorile ObjectId
Cum să structurezi și să testezi API-ul și apoi să îl conectezi la un frontend
Poți găsi codul complet pentru acest tutorial pe GitHub, dacă preferi să îl clonezi și să citești pe parcurs.
Cerințe preliminare
Înainte să începi, ar trebui să ai:
Node.js 18+ instalat (npm este inclus implicit)
MongoDB local sau un cluster MongoDB Atlas gratuit
Postman sau orice client HTTP
Cunoștințe de bază de JavaScript și concepte REST
Pasul 1: Configurează proiectul
Hai să construim managerul nostru simplu de sarcini. Mai întâi vom crea un folder nou și vom inițializa un proiect Node rulând comenzile de mai jos în terminal:
mkdir task-manager-categories
cd task-manager-categories
npm init -yFuncționează cu
Apoi vom instala cele patru dependențe runtime de care are nevoie aplicația: express pentru rutare, driverul oficial MongoDB, dotenv pentru încărcarea variabilelor de mediu și, în final, helmet pentru antete de securitate.
Pentru dezvoltare, instalează nodemon ca dependență de dev cu comanda de mai jos, astfel încât serverul să se reîncarce automat când se schimbă fișierele:
Apoi, deschide folderul proiectului în IDE și actualizează secțiunea scripts din package.json pentru a include următoarele. Astfel poți porni serverul cu npm start sau îl poți rula în modul de dezvoltare cu npm run dev:
"scripts":{"start":"node server.js","dev":"nodemon server.js"}Funcționează cu
Iată structura pe care o vom construi pe parcurs. Fiecare folder are un rol clar: db/ se ocupă de conexiunea la bază, lib/ conține funcții helper, middleware/ este pentru middleware Express, iar routes/ definește handler-ele pentru requesturi. Folderul public/ va conține un mic frontend pe care îl vom adăuga mai târziu. Chiar și într-un proiect mic, această structură menține lucrurile ușor de urmărit și clarifică unde să adaugi cod nou. Poți crea acum scheletul de foldere și să-l completezi pe parcurs sau poți sări peste asta și să adaugi fiecare fișier pe măsură ce parcurgem pașii:
Pentru a stoca și a prelua sarcini, mai întâi trebuie să conectăm aplicația la MongoDB. Asta se face folosind un connection string, care îi spune driverului MongoDB cum să se conecteze la baza ta de date. În loc să îl pui direct în cod, e mai bine să-l păstrezi într-un fișier de mediu. Astfel, valorile sensibile rămân în afara codului și e mai ușor să comuți între medii.
Creează un fișier .env.example pentru a documenta variabilele necesare și un fișier .env pentru valorile tale reale, astfel:
# Copy this file to `.env` and fill in real values.# --- Local MongoDB ---# Use this if you're running MongoDB locallyMONGO_URI=mongodb://127.0.0.1:27017
# --- MongoDB Atlas ---# Replace <username>, <password>, and <cluster-url> with your actual values# Example: mongodb+srv://user:[email protected]/# MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majorityDB_NAME=taskmanager
PORT=3000Funcționează cu
Acum că avem un connection string, îl putem folosi pentru a configura conexiunea la MongoDB. Creează un fișier db/connect.js. Aici vom inițializa clientul MongoDB și îl vom face disponibil în restul aplicației:
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 };Funcționează cu
Acest fișier configurează un singur client MongoDB pe care îl poate reutiliza restul aplicației. Vrei să-l creezi o singură dată, deoarece driverul se ocupă deja de connection pooling. Crearea unui client nou la fiecare request poate părea ok la început, dar va cauza destul de repede probleme de performanță.
Pasul 3: Definește structura și validarea sarcinii
În acest punct, aplicația se poate conecta la MongoDB. Acum trebuie să definim cum arată, de fapt, o sarcină înainte să începem să salvăm ceva.
Aici folosirea driverului nativ MongoDB începe să se simtă puțin diferit. Nu există un fișier de schemă ca în Mongoose. În schimb, „schema” este pur și simplu forma obiectului pe care îl inserezi în bază. Poate părea mai lejer la început, dar e util, pentru că rămâi aproape de cum funcționează MongoDB în realitate, fără abstracții care ascund detalii.
Totuși, vrem în continuare validare. Așa că, în loc să împrăștiem logica prin rute diferite, o vom păstra într-un singur loc, astfel încât totul să urmeze aceleași reguli. Creează un fișier numit lib/taskDocument.js și adaugă codul de mai jos:
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
};Funcționează cu
Acest fișier acționează ca un portar pentru orice ajunge în baza ta de date. Fiecare cerere de creare sau actualizare trece pe aici, astfel încât regulile sunt impuse într-un singur loc.
Se asigură că sarcinile au mereu forma corectă, păstrează categoriile consecvente și evită surprizele atunci când începi să interoghezi datele. ValidationError personalizat îți oferă și o modalitate clară de a separa inputul greșit de problemele reale ale serverului, astfel încât API-ul tău să poată răspunde adecvat. Cu asta pus la punct, restul aplicației poate rămâne simplu. Fiecare rută se poate concentra pe treaba ei, știind că datele primite sunt deja valide. Următorul pas: conectăm rutele și începem să salvăm sarcini în MongoDB.
Pasul 4: Gestionează ObjectId și validarea cererilor
Acum că știm cum arată o sarcină, să vedem cum o referim. Ori de câte ori o rută include un parametru :id, acesta vine ca un string simplu. Dar MongoDB se așteaptă la un ObjectId. Dacă stringul e greșit formatat, driverul aruncă o eroare destul de neclară. În loc să gestionăm asta în fiecare rută, vom centraliza prin middleware, astfel încât fiecare endpoint să se comporte la fel.
Pentru asta, creează un fișier numit middleware/parseObjectId.js și adaugă următoarele:
const{ ObjectId }=require('mongodb');functionparseObjectId(req, res, next){const{ id }= req.params;if(!ObjectId.isValid(id)){return res.status(400).json({error:'invalid task id'});}
req.taskId =newObjectId(id);next();}
module.exports = parseObjectId;Funcționează cu
Acest middleware rulează înaintea handlerului de rută. Verifică dacă id este valid, îl convertește într-un ObjectId și îl atașează la req.taskId. Astfel, până ajunge logica din rută să ruleze, lucrezi deja cu un ObjectId corect, iar inputurile greșite sunt respinse devreme cu un răspuns clar 400. În continuare, îl vom conecta la rutele noastre și vom pune totul cap la cap.
Pasul 5: Construiește rutele pentru sarcini (CRUD + filtrare)
În acest punct, mare parte din treabă e deja făcută. Am definit cum arată o sarcină validă și am gestionat modul în care ID-urile sunt analizate și validate. Asta înseamnă că handler-ele de rute se pot concentra pe un singur lucru: să vorbească cu baza de date.
Hai să construim rutele reale ale API-ului, creând un fișier numit routes/tasks.js și adăugând următoarele:
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;Funcționează cu
Fiecare rută din codul de mai sus urmează același tipar: ia inputul din request, îl trece prin helper-ele construite anterior, apelează MongoDB și apoi returnează un răspuns. Cu validarea și parsarea ID-ului deja gestionate, codul rămâne scurt și previzibil. În practică, iată cum funcționează:
Ruta POST construiește o sarcină nouă folosind buildTaskDocument înainte de a o insera
Rutele GET filtrează opțional după categorie și returnează rezultatele sortate de la cele mai noi
Ruta PUT folosește buildTaskUpdate, astfel încât actualizările parțiale sunt sigure și consecvente
Ruta DELETE elimină o sarcină după ObjectId deja analizat
Mai sunt câteva detalii în acest cod care merită menționate:
find() returnează un cursor, nu un array, așa că înlănțuim .toArray() după sortare pentru a obține efectiv rezultatele
findOneAndUpdate cu returnDocument: 'after' îți oferă imediat documentul actualizat
Returnăm coduri HTTP corecte: 201 pentru creare, 204 pentru ștergere și 400 sau 404 acolo unde e cazul
Erorile neașteptate sunt transmise către next(err), pentru a fi gestionate într-un singur loc, nu în fiecare rută
Pasul 6: Conectează totul în server
Acum toate piesele sunt la locul lor. Avem validare, rute curate și o conexiune funcțională la bază. Mai trebuie doar să legăm totul și să pornim serverul. Creează un fișier server.js. Acesta este punctul de intrare al aplicației, unde se adună toate:
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 };Funcționează cu
Acest fișier pune totul cap la cap într-un mod curat. Verifică dacă variabilele de mediu sunt setate înainte de a rula orice, aplică securitate de bază cu helmet și limitează JSON-ul primit, astfel încât aplicația ta să nu accepte din greșeală payloaduri uriașe. De asemenea, servește folderul public/, astfel încât frontendul să poată rula în aceeași aplicație, fără un server separat.
Toate rutele tale sunt montate sub /tasks, iar orice nu se potrivește returnează un 404 curat. Erorile sunt gestionate într-un singur loc, nu repetate în fiecare rută, ceea ce păstrează consecvența și mentenanța ușoară. Conexiunea la bază este stabilită înainte ca serverul să înceapă să asculte, astfel încât nu accepți cereri înainte de a fi pregătit, iar oprirea este gestionată grațios, închizând corect conexiunile.
Acum pornește totul și verifică dacă funcționează rulând npm run dev. Dacă totul este conectat corect, ar trebui să vezi ceva de genul:
MongoDB connected (db: taskmanager)
Server running on http://localhost:3000
Funcționează cu
În acest punct, totul este pornit și funcțional! Avem acum un API complet funcțional, așa că hai să îl testăm.
Pasul 7: Testează endpointurile API-ului
Înainte de a construi o interfață, merită să confirmi că fiecare endpoint funcționează de unul singur. Asta face depanarea mult mai ușoară, deoarece poți spune rapid dacă o problemă vine din API sau din frontend. Vom folosi Postman (sau orice client HTTP) și vom rula câteva cereri în ordine. Fiecare se bazează pe datele create de pasul anterior.
Creează o sarcină: Trimite o cerere POST cu un title, description și category. Dacă totul merge, API-ul returnează 201 Created cu noua sarcină și _id generat.
POST http://localhost:3000/tasks
Content-Type: application/json
{"title":"Write GeeksForGeeks article",
"description":"First draft by Friday",
"category":"Work"}Funcționează cu
Creează câteva în plus: Rulează aceeași cerere POST cu corpuri diferite, ca să ai destule date pentru listare și filtrare. După asta, vei avea un mic set de date pentru interogare.
{"title":"Go for a run","category":"Personal"}{"title":"Read MongoDB docs","category":"Study"}{"title":"Buy groceries"}// category defaults to "Other"Funcționează cu
Listează toate sarcinile: Aceasta returnează toate sarcinile, sortate de la cele mai noi.
Filtrează după categorie: Aceasta returnează doar sarcinile din Work. Indexul adăugat mai devreme menține această interogare rapidă pe măsură ce datele cresc.
GET http://localhost:3000/tasks?category=WorkFuncționează cu
Actualizează o sarcină: Folosește _id de la una dintre sarcinile tocmai create (îl poți copia din răspunsul la POST sau din endpointul de listare). Aceasta returnează sarcina actualizată. Trebuie să trimiți doar câmpurile pe care vrei să le schimbi, astfel încât actualizările parțiale rămân simple.
PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json
{"completed":true}Funcționează cu
Șterge o sarcină: Folosește _id de la sarcina pe care vrei să o ștergi. Aceasta returnează 204 No Content. Dacă încerci să obții din nou aceeași sarcină, vei primi un 404 cu "task not found".
DELETE http://localhost:3000/tasks/<paste-task-id-here>Funcționează cu
În acest punct, ai confirmat că toate rutele se comportă așa cum trebuie. API-ul își face treaba, așa că putem trece la construirea unei interfețe peste el.
Pasul 8: Adaugă un frontend pentru a interacționa cu API-ul
Postman e grozav pentru a verifica API-ul, dar ar fi plăcut să vezi cum funcționează pe un site real, așa că hai să adăugăm ceva cod de frontend în aplicație.
În loc să lipesc aici fișiere HTML și CSS mari, am adăugat codul în acest repo GitHub. Mergi acolo și copiază conținutul fișierelor index.html, styles.css și favicon.svg în folderul public/ din rădăcina proiectului tău. Salvează modificările și apoi mergi la localhost:3000. Apoi lipește codul de mai jos în fișierul app.js:
const form = document.getElementById('task-form');const titleEl = document.getElementById('title');const descEl = document.getElementById('description');const catEl = document.getElementById('category');const errEl = document.getElementById('form-error');const listEl = document.getElementById('task-list');const emptyEl = document.getElementById('empty');const filterEl = document.getElementById('filter');const countEl = document.getElementById('task-count');asyncfunctionapi(method, path, body){const res =awaitfetch(path,{
method,headers: body ?{'Content-Type':'application/json'}:{},body: body ?JSON.stringify(body):undefined});if(res.status ===204)returnnull;const data =await res.json();if(!res.ok)thrownewError(data.error ||`Request failed (${res.status})`);return data;}constfetchTasks=(category)=>api('GET','/tasks'+(category ?`?category=${encodeURIComponent(category)}`:''));constcreateTask=(body)=>api('POST','/tasks', body);constupdateTask=(id, body)=>api('PUT',`/tasks/${id}`, body);constdeleteTask=(id)=>api('DELETE',`/tasks/${id}`);functionupdateCount(tasks){const active = tasks.filter((t)=>!t.completed).length;const total = tasks.length;if(total ===0){
countEl.textContent ='';return;}const scope = filterEl.value ?` in ${filterEl.value}`:'';
countEl.textContent =`${total} tasks${scope} · ${active} active`;}functionrenderTask(task){const li = document.createElement('li');
li.className ='task'+(task.completed ?' completed':'');const checkbox = document.createElement('input');
checkbox.type ='checkbox';
checkbox.checked = task.completed;
checkbox.addEventListener('change',()=>updateTask(task._id,{completed: checkbox.checked }).then(refresh));const title = document.createElement('div');
title.textContent = task.title;const badge = document.createElement('span');
badge.textContent = task.category;const del = document.createElement('button');
del.textContent ='Delete';
del.onclick=()=>deleteTask(task._id).then(refresh);
li.append(checkbox, title, badge, del);return li;}functionrender(tasks){
listEl.innerHTML ='';
emptyEl.classList.toggle('hidden', tasks.length >0);for(const task of tasks){
listEl.appendChild(renderTask(task));}updateCount(tasks);}asyncfunctionrefresh(){try{const tasks =awaitfetchTasks(filterEl.value);render(tasks);}catch(e){
errEl.textContent = e.message;}}
form.addEventListener('submit',async(e)=>{
e.preventDefault();
errEl.textContent ='';const title = titleEl.value.trim();if(!title){
errEl.textContent ='Title is required.';return;}try{awaitcreateTask({
title,description: descEl.value.trim(),category: catEl.value
});
form.reset();awaitrefresh();}catch(e){
errEl.textContent = e.message;}});
filterEl.addEventListener('change', refresh);refresh();Funcționează cu
Codul JavaScript este locul în care totul se reconectează la API-ul pe care l-ai construit. Fiecare funcție corespunde uneia dintre rutele tale, ceea ce păstrează frontendul ușor de urmărit. Toate cererile trec și printr-un singur helper api(), astfel încât gestionezi erorile într-un singur loc, nu repeți aceeași logică peste tot.
Filtrarea funcționează prin trecerea unui query string precum ?category=Work, care se leagă direct de logica din backend pe care ai adăugat-o deja. După crearea, actualizarea sau ștergerea unei sarcini, aplicația preia din nou lista actualizată, astfel încât UI-ul rămâne în sincron. Contorul de sarcini e un detaliu mic, dar face aplicația să pară mai vie în utilizare.
Acum repornește serverul cu npm run dev, apoi deschide http://localhost:3000 pentru a încerca Task Manager-ul. Adaugă câteva sarcini, actualizează-le, comută între categorii și șterge unele. În acest punct, frontendul și backendul sunt complet conectate și funcționează cap-coadă.
Rezumat
Felicitări! Ai construit cu succes un API complet pentru gestionarea sarcinilor de la zero folosind Node.js, Express și MongoDB. Ai gestionat validarea, ai păstrat rutele concise și ai conectat totul într-un sistem funcțional. Mai important, ai rămas aproape de modul în care funcționează efectiv MongoDB, fără a ascunde totul în spatele abstracțiilor. De aici, poți începe să evoluezi aplicația pentru producție.
Idei principale
Folosirea middleware-ului și centralizarea validării îți păstrează codul curat și previzibil.
Driverul nativ MongoDB îți oferă control fără abstracții inutile.
Un API bine structurat e ușor de extins într-o aplicație gata de producție.
Întrebări frecvente
Am nevoie de Mongoose pentru a construi un API MongoDB?⌄
Nu. Acest tutorial folosește driverul nativ MongoDB, care îți oferă mai mult control și păstrează lucrurile suple. Poți adăuga Mongoose mai târziu dacă vrei abstracții de tip schemă.
Cum validez datele fără o schemă?⌄
Poți centraliza validarea în funcții helper (cum ar fi buildTaskDocument), asigurându-te că fiecare rută impune aceleași reguli înainte de a scrie în bază.
Ce se întâmplă dacă trimit un ObjectId invalid?⌄
Middleware-ul o respinge devreme cu un răspuns 400, prevenind erori confuze aruncate de MongoDB.
O listă simplă de to-do merge perfect la început: adaugi câteva sarcini și le poți bifa după ce le termini. Dar pe măsură ce sarcinile se înmulțesc, devine greu de folosit. Un lucru care poate rezolva asta este adăugarea de categorii, astfel încât fiecare sarcină să aparțină unor categorii precum Work, Study, Personal etc., iar tu să poți filtra lista după categoria dorită.
În acest tutorial, vei învăța cum să construiești un API REST care îți permite să organizezi sarcinile pe categorii folosind Node.js și MongoDB. La final, vei avea un server Node.js funcțional cu cinci endpointuri: create, list, get, update și delete, plus un filtru după categorie. Vei vedea, de asemenea, cum să impui validarea categoriilor folosind driverul nativ MongoDB, fără să te bazezi pe un ODM precum Mongoose.
Ce vei învăța
Poți găsi codul complet pentru acest tutorial pe GitHub, dacă preferi să îl clonezi și să citești pe parcurs.
Cerințe preliminare
Înainte să începi, ar trebui să ai:
Pasul 1: Configurează proiectul
Hai să construim managerul nostru simplu de sarcini. Mai întâi vom crea un folder nou și vom inițializa un proiect Node rulând comenzile de mai jos în terminal:
Apoi vom instala cele patru dependențe runtime de care are nevoie aplicația: express pentru rutare, driverul oficial MongoDB, dotenv pentru încărcarea variabilelor de mediu și, în final, helmet pentru antete de securitate.
Pentru dezvoltare, instalează nodemon ca dependență de dev cu comanda de mai jos, astfel încât serverul să se reîncarce automat când se schimbă fișierele:
Apoi, deschide folderul proiectului în IDE și actualizează secțiunea scripts din package.json pentru a include următoarele. Astfel poți porni serverul cu npm start sau îl poți rula în modul de dezvoltare cu npm run dev:
Iată structura pe care o vom construi pe parcurs. Fiecare folder are un rol clar: db/ se ocupă de conexiunea la bază, lib/ conține funcții helper, middleware/ este pentru middleware Express, iar routes/ definește handler-ele pentru requesturi. Folderul public/ va conține un mic frontend pe care îl vom adăuga mai târziu. Chiar și într-un proiect mic, această structură menține lucrurile ușor de urmărit și clarifică unde să adaugi cod nou. Poți crea acum scheletul de foldere și să-l completezi pe parcurs sau poți sări peste asta și să adaugi fiecare fișier pe măsură ce parcurgem pașii:
Pasul 2: Conectează-te la MongoDB
Pentru a stoca și a prelua sarcini, mai întâi trebuie să conectăm aplicația la MongoDB. Asta se face folosind un connection string, care îi spune driverului MongoDB cum să se conecteze la baza ta de date. În loc să îl pui direct în cod, e mai bine să-l păstrezi într-un fișier de mediu. Astfel, valorile sensibile rămân în afara codului și e mai ușor să comuți între medii.
Creează un fișier .env.example pentru a documenta variabilele necesare și un fișier .env pentru valorile tale reale, astfel:
Acum că avem un connection string, îl putem folosi pentru a configura conexiunea la MongoDB. Creează un fișier db/connect.js. Aici vom inițializa clientul MongoDB și îl vom face disponibil în restul aplicației:
Acest fișier configurează un singur client MongoDB pe care îl poate reutiliza restul aplicației. Vrei să-l creezi o singură dată, deoarece driverul se ocupă deja de connection pooling. Crearea unui client nou la fiecare request poate părea ok la început, dar va cauza destul de repede probleme de performanță.
Pasul 3: Definește structura și validarea sarcinii
În acest punct, aplicația se poate conecta la MongoDB. Acum trebuie să definim cum arată, de fapt, o sarcină înainte să începem să salvăm ceva.
Aici folosirea driverului nativ MongoDB începe să se simtă puțin diferit. Nu există un fișier de schemă ca în Mongoose. În schimb, „schema” este pur și simplu forma obiectului pe care îl inserezi în bază. Poate părea mai lejer la început, dar e util, pentru că rămâi aproape de cum funcționează MongoDB în realitate, fără abstracții care ascund detalii.
Totuși, vrem în continuare validare. Așa că, în loc să împrăștiem logica prin rute diferite, o vom păstra într-un singur loc, astfel încât totul să urmeze aceleași reguli. Creează un fișier numit lib/taskDocument.js și adaugă codul de mai jos:
Acest fișier acționează ca un portar pentru orice ajunge în baza ta de date. Fiecare cerere de creare sau actualizare trece pe aici, astfel încât regulile sunt impuse într-un singur loc.
Se asigură că sarcinile au mereu forma corectă, păstrează categoriile consecvente și evită surprizele atunci când începi să interoghezi datele. ValidationError personalizat îți oferă și o modalitate clară de a separa inputul greșit de problemele reale ale serverului, astfel încât API-ul tău să poată răspunde adecvat. Cu asta pus la punct, restul aplicației poate rămâne simplu. Fiecare rută se poate concentra pe treaba ei, știind că datele primite sunt deja valide. Următorul pas: conectăm rutele și începem să salvăm sarcini în MongoDB.
Pasul 4: Gestionează ObjectId și validarea cererilor
Acum că știm cum arată o sarcină, să vedem cum o referim. Ori de câte ori o rută include un parametru :id, acesta vine ca un string simplu. Dar MongoDB se așteaptă la un ObjectId. Dacă stringul e greșit formatat, driverul aruncă o eroare destul de neclară. În loc să gestionăm asta în fiecare rută, vom centraliza prin middleware, astfel încât fiecare endpoint să se comporte la fel.
Pentru asta, creează un fișier numit middleware/parseObjectId.js și adaugă următoarele:
Acest middleware rulează înaintea handlerului de rută. Verifică dacă id este valid, îl convertește într-un ObjectId și îl atașează la req.taskId. Astfel, până ajunge logica din rută să ruleze, lucrezi deja cu un ObjectId corect, iar inputurile greșite sunt respinse devreme cu un răspuns clar 400. În continuare, îl vom conecta la rutele noastre și vom pune totul cap la cap.
Pasul 5: Construiește rutele pentru sarcini (CRUD + filtrare)
În acest punct, mare parte din treabă e deja făcută. Am definit cum arată o sarcină validă și am gestionat modul în care ID-urile sunt analizate și validate. Asta înseamnă că handler-ele de rute se pot concentra pe un singur lucru: să vorbească cu baza de date.
Hai să construim rutele reale ale API-ului, creând un fișier numit routes/tasks.js și adăugând următoarele:
Fiecare rută din codul de mai sus urmează același tipar: ia inputul din request, îl trece prin helper-ele construite anterior, apelează MongoDB și apoi returnează un răspuns. Cu validarea și parsarea ID-ului deja gestionate, codul rămâne scurt și previzibil. În practică, iată cum funcționează:
Mai sunt câteva detalii în acest cod care merită menționate:
Pasul 6: Conectează totul în server
Acum toate piesele sunt la locul lor. Avem validare, rute curate și o conexiune funcțională la bază. Mai trebuie doar să legăm totul și să pornim serverul. Creează un fișier server.js. Acesta este punctul de intrare al aplicației, unde se adună toate:
Acest fișier pune totul cap la cap într-un mod curat. Verifică dacă variabilele de mediu sunt setate înainte de a rula orice, aplică securitate de bază cu helmet și limitează JSON-ul primit, astfel încât aplicația ta să nu accepte din greșeală payloaduri uriașe. De asemenea, servește folderul public/, astfel încât frontendul să poată rula în aceeași aplicație, fără un server separat.
Toate rutele tale sunt montate sub /tasks, iar orice nu se potrivește returnează un 404 curat. Erorile sunt gestionate într-un singur loc, nu repetate în fiecare rută, ceea ce păstrează consecvența și mentenanța ușoară. Conexiunea la bază este stabilită înainte ca serverul să înceapă să asculte, astfel încât nu accepți cereri înainte de a fi pregătit, iar oprirea este gestionată grațios, închizând corect conexiunile.
Acum pornește totul și verifică dacă funcționează rulând npm run dev. Dacă totul este conectat corect, ar trebui să vezi ceva de genul:
În acest punct, totul este pornit și funcțional! Avem acum un API complet funcțional, așa că hai să îl testăm.
Pasul 7: Testează endpointurile API-ului
Înainte de a construi o interfață, merită să confirmi că fiecare endpoint funcționează de unul singur. Asta face depanarea mult mai ușoară, deoarece poți spune rapid dacă o problemă vine din API sau din frontend. Vom folosi Postman (sau orice client HTTP) și vom rula câteva cereri în ordine. Fiecare se bazează pe datele create de pasul anterior.
Creează o sarcină: Trimite o cerere POST cu un title, description și category. Dacă totul merge, API-ul returnează 201 Created cu noua sarcină și _id generat.
Creează câteva în plus: Rulează aceeași cerere POST cu corpuri diferite, ca să ai destule date pentru listare și filtrare. După asta, vei avea un mic set de date pentru interogare.
Listează toate sarcinile: Aceasta returnează toate sarcinile, sortate de la cele mai noi.
Filtrează după categorie: Aceasta returnează doar sarcinile din Work. Indexul adăugat mai devreme menține această interogare rapidă pe măsură ce datele cresc.
Actualizează o sarcină: Folosește _id de la una dintre sarcinile tocmai create (îl poți copia din răspunsul la POST sau din endpointul de listare). Aceasta returnează sarcina actualizată. Trebuie să trimiți doar câmpurile pe care vrei să le schimbi, astfel încât actualizările parțiale rămân simple.
Șterge o sarcină: Folosește _id de la sarcina pe care vrei să o ștergi. Aceasta returnează 204 No Content. Dacă încerci să obții din nou aceeași sarcină, vei primi un 404 cu "task not found".
În acest punct, ai confirmat că toate rutele se comportă așa cum trebuie. API-ul își face treaba, așa că putem trece la construirea unei interfețe peste el.
Pasul 8: Adaugă un frontend pentru a interacționa cu API-ul
Postman e grozav pentru a verifica API-ul, dar ar fi plăcut să vezi cum funcționează pe un site real, așa că hai să adăugăm ceva cod de frontend în aplicație.
În loc să lipesc aici fișiere HTML și CSS mari, am adăugat codul în acest repo GitHub. Mergi acolo și copiază conținutul fișierelor index.html, styles.css și favicon.svg în folderul public/ din rădăcina proiectului tău. Salvează modificările și apoi mergi la localhost:3000. Apoi lipește codul de mai jos în fișierul app.js:
Codul JavaScript este locul în care totul se reconectează la API-ul pe care l-ai construit. Fiecare funcție corespunde uneia dintre rutele tale, ceea ce păstrează frontendul ușor de urmărit. Toate cererile trec și printr-un singur helper api(), astfel încât gestionezi erorile într-un singur loc, nu repeți aceeași logică peste tot.
Filtrarea funcționează prin trecerea unui query string precum ?category=Work, care se leagă direct de logica din backend pe care ai adăugat-o deja. După crearea, actualizarea sau ștergerea unei sarcini, aplicația preia din nou lista actualizată, astfel încât UI-ul rămâne în sincron. Contorul de sarcini e un detaliu mic, dar face aplicația să pară mai vie în utilizare.
Acum repornește serverul cu npm run dev, apoi deschide http://localhost:3000 pentru a încerca Task Manager-ul. Adaugă câteva sarcini, actualizează-le, comută între categorii și șterge unele. În acest punct, frontendul și backendul sunt complet conectate și funcționează cap-coadă.
Rezumat
Felicitări! Ai construit cu succes un API complet pentru gestionarea sarcinilor de la zero folosind Node.js, Express și MongoDB. Ai gestionat validarea, ai păstrat rutele concise și ai conectat totul într-un sistem funcțional. Mai important, ai rămas aproape de modul în care funcționează efectiv MongoDB, fără a ascunde totul în spatele abstracțiilor. De aici, poți începe să evoluezi aplicația pentru producție.
Idei principale