Przejdź do głównej treści

Zbuduj prosty menedżer zadań z kategoriami w Node.js i MongoDB

Naucz się organizować zadania według kategorii z użyciem Node.js i MongoDB. Zbuduj REST API z endpointami create, list, get, update, delete oraz filtrem kategorii.
Zaktualizowano 10 wrz 2026  · 10 min Czytać

Eksploruj z AI

ChatGPTClaudePerplexity

Zwykła lista to-do na początku działa świetnie: dodajesz kilka zadań i zamykasz je po wykonaniu. Ale wraz ze wzrostem liczby zadań korzystanie z niej staje się uciążliwe. Jednym ze sposobów na poprawę jest dodanie kategoryzacji, dzięki której każde zadanie może należeć do kategorii takich jak Praca, Nauka, Osobiste itp., a ty możesz filtrować listę według wybranej kategorii.

W tym samouczku nauczysz się budować REST API, które pozwala organizować zadania według kategorii za pomocą Node.js i MongoDB. Na końcu będziesz mieć działający serwer Node.js z pięcioma endpointami: create, list, get, update i delete, a także filtrem kategorii. Zobaczysz też, jak egzekwować walidację kategorii przy użyciu natywnego sterownika MongoDB, bez polegania na ODM-ie takim jak Mongoose.

Czego się nauczysz

  • Jak zbudować REST API z Node.js, Express i MongoDB, włącznie z pełnymi endpointami CRUD
  • Jak walidować dane wejściowe, egzekwować kategorie i bezpiecznie obsługiwać wartości ObjectId
  • Jak zaprojektować i przetestować API, a następnie połączyć je z frontendem

Pełny kod do tego samouczka znajdziesz na GitHubie — możesz go sklonować i czytać wraz z postępem.

Wymagania wstępne

Zanim zaczniesz, powinieneś mieć:

  • Zainstalowany Node.js 18+ (npm jest domyślnie dołączony)
  • Działającą lokalnie bazę MongoDB lub darmowy klaster MongoDB Atlas
  • Postmana lub dowolnego klienta HTTP
  • Podstawową znajomość JavaScriptu i koncepcji REST

Krok 1: Przygotuj projekt

Przejdźmy do budowy prostego menedżera zadań. Najpierw utworzymy nowy folder i zainicjalizujemy projekt node, uruchamiając poniższe polecenia w terminalu:

mkdir task-manager-categories
cd task-manager-categories
npm init -y

Następnie zainstalujemy cztery zależności runtime potrzebne do działania aplikacji: express do routingu, oficjalny sterownik MongoDB, dotenv do ładowania zmiennych środowiskowych oraz helmet do nagłówków bezpieczeństwa.

npm install express mongodb dotenv helmet

Na potrzeby developmentu zainstaluj nodemon jako zależność deweloperską poleceniem poniżej, aby serwer przeładowywał się automatycznie po zmianach w plikach:

npm install --save-dev nodemon

Otwórz teraz folder projektu w swoim IDE i zaktualizuj sekcję scripts w pliku package.json, aby zawierała poniższe. Dzięki temu uruchomisz serwer poleceniem npm start lub w trybie deweloperskim npm run dev:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

Oto struktura, którą zbudujemy po drodze. Każdy folder ma jasną rolę: db/ odpowiada za połączenie z bazą, lib/ zawiera funkcje pomocnicze, middleware/ to middleware dla Expressa, a routes/ definiuje obsługę żądań. Folder public/ będzie przechowywał mały frontend, który dodamy później. Nawet w małym projekcie taka struktura ułatwia nawigację i jasno wskazuje, gdzie powinien trafić nowy kod. Możesz teraz utworzyć układ folderów i uzupełniać go w trakcie, albo pominąć to i dodawać każdy plik po kolei, zgodnie z krokami:

task-manager-categories/
├── db/
│ └── connect.js
├── lib/
│ └── taskDocument.js
├── middleware/
│ └── parseObjectId.js
├── routes/
│ └── tasks.js
├── public/
│ ├── index.html
│ ├── styles.css
│ └── app.js
├── .env
├── .env.example
├── .gitignore
├── package.json
└── server.js

Krok 2: Połącz się z MongoDB

Aby przechowywać i pobierać zadania, musimy najpierw połączyć aplikację z MongoDB. Robi się to za pomocą connection stringa, który mówi sterownikowi MongoDB, jak połączyć się z twoją bazą. Zamiast wpisywać go na sztywno w kodzie, lepiej trzymać go w pliku środowiskowym. Dzięki temu wrażliwe wartości nie trafią do repozytorium, a przełączanie środowisk będzie prostsze.

Utwórz plik .env.example, aby udokumentować wymagane zmienne, oraz plik .env dla twoich właściwych wartości, jak poniżej:

# Copy this file to `.env` and fill in real values.

# --- Local MongoDB ---
# Use this if you're running MongoDB locally
MONGO_URI=mongodb://127.0.0.1:27017

# --- MongoDB Atlas ---
# Replace <username>, <password>, and <cluster-url> with your actual values
# Example: mongodb+srv://user:pass@cluster0.abcde.mongodb.net/
# MONGO_URI=mongodb+srv://<username>:<password>@<cluster-url>/?retryWrites=true&w=majority

DB_NAME=taskmanager
PORT=3000

Skoro mamy już connection string, możemy użyć go do skonfigurowania połączenia z MongoDB. Utwórz plik db/connect.js. Tutaj zainicjalizujemy klienta MongoDB i udostępnimy go reszcie aplikacji:

const { MongoClient } = require("mongodb");

let client;
let db;

async function connectDB() {
  if (!process.env.MONGO_URI) {
    throw new Error("MONGO_URI is not set. Check your .env file.");
  }
  client = new MongoClient(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})`);
}

function getTasksCollection() {
  if (!db) {
    throw new Error("Database not initialized. Call connectDB() first.");
  }
  return db.collection("tasks");
}

async function closeDB() {
  if (client) await client.close();
}

module.exports = { connectDB, getTasksCollection, closeDB };

Ten plik tworzy pojedynczego klienta MongoDB, którego reszta aplikacji może ponownie używać. Chcesz go utworzyć tylko raz, ponieważ sterownik i tak zarządza pulą połączeń pod spodem. Tworzenie nowego klienta przy każdym żądaniu może na początku wydawać się w porządku, ale szybko spowoduje problemy z wydajnością.

Krok 3: Zdefiniuj strukturę zadania i walidację

W tym momencie aplikacja potrafi połączyć się z MongoDB. Teraz musimy zdefiniować, jak właściwie wygląda zadanie, zanim zaczniemy cokolwiek zapisywać.

Tutaj zaczyna być widać różnicę przy korzystaniu z natywnego sterownika MongoDB. Nie ma pliku schematu jak w Mongoose. Zamiast tego „schematem” jest po prostu kształt obiektu, który wstawiasz do bazy. Może to początkowo wydawać się luźne, ale jest pomocne, bo pozostajesz blisko tego, co MongoDB naprawdę robi, bez ukrywania logiki za abstrakcjami.

Walidacji jednak nadal potrzebujemy. Zamiast rozpraszać tę logikę po różnych trasach, utrzymamy ją w jednym miejscu, aby wszystko podlegało tym samym regułom. Utwórz plik o nazwie lib/taskDocument.js i dodaj do niego poniższy kod:

class ValidationError extends Error {
  constructor(message) {
    super(message);
    this.name = 'ValidationError';
  }
}

const ALLOWED_CATEGORIES = ['Work', 'Personal', 'Study', 'Other'];

function assertValidCategory(category) {
  if (!ALLOWED_CATEGORIES.includes(category)) {
    throw new ValidationError(
      `category must be one of: ${ALLOWED_CATEGORIES.join(', ')}`
    );
  }
}

function buildTaskDocument(body = {}) {
  if (!body.title || typeof body.title !== 'string' || !body.title.trim()) {
    throw new ValidationError('title is required and must be a non-empty string');
  }
  if (body.category != null) {
    assertValidCategory(body.category);
  }
  const now = new Date();
  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
  };
}

function buildTaskUpdate(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) {
    throw new ValidationError('no valid fields provided for update');
  }
  updates.updatedAt = new Date();
  return updates;
}

module.exports = {
  ALLOWED_CATEGORIES,
  buildTaskDocument,
  buildTaskUpdate,
  ValidationError
};

Ten plik działa jak strażnik dla wszystkiego, co trafia do bazy. Każde żądanie utworzenia lub aktualizacji przechodzi tędy, więc jedno miejsce egzekwuje reguły.

Dba o to, by zadania miały zawsze właściwy kształt, utrzymuje spójność kategorii i unika niespodzianek przy zapytaniach do danych. Własny ValidationError daje też czysty sposób oddzielenia złych danych wejściowych od prawdziwych problemów serwera, dzięki czemu API może odpowiednio reagować. Mając to na miejscu, reszta aplikacji może pozostać prosta. Każda trasa skupia się na swojej pracy, wiedząc, że dane są już poprawne. Następnie podłączymy trasy i zaczniemy zapisywać zadania w MongoDB.

Krok 4: Obsłuż ObjectId i walidację żądań

Skoro wiemy już, jak wygląda zadanie, zajmijmy się sposobem jego referencjonowania. Za każdym razem, gdy trasa zawiera parametr :id, trafia on jako zwykły string. Ale MongoDB oczekuje ObjectId. Jeśli string jest źle sformatowany, sterownik wyrzuci mało pomocny błąd. Zamiast obsługiwać to w każdej trasie, scentralizujemy to middleware’em, aby każdy endpoint zachowywał się tak samo.

Aby to zrobić, utwórz plik middleware/parseObjectId.js i dodaj do niego poniższe:

const { ObjectId } = require('mongodb');

function parseObjectId(req, res, next) {
  const { id } = req.params;
  if (!ObjectId.isValid(id)) {
    return res.status(400).json({ error: 'invalid task id' });
  }
  req.taskId = new ObjectId(id);
  next();
}

module.exports = parseObjectId;

Ten middleware uruchamia się przed handlerem trasy. Sprawdza, czy id jest poprawne, konwertuje je na ObjectId i dołącza do req.taskId. Dzięki temu, gdy logika trasy się uruchamia, zawsze pracujesz z prawidłowym ObjectId, a złe dane wejściowe są odrzucane wcześnie z jasną odpowiedzią 400. Dalej podłączymy to do naszych tras i wszystko ze sobą zwiążemy.

Krok 5: Zbuduj trasy zadań (CRUD + filtrowanie)

Na tym etapie większość ciężkiej pracy jest już wykonana. Zdefiniowaliśmy, jak wygląda poprawne zadanie, i obsłużyliśmy parsowanie oraz walidację identyfikatorów. Dzięki temu handlery tras mogą skupić się na jednym: rozmowie z bazą danych.

Zbudujmy właściwe trasy API, tworząc plik routes/tasks.js i dodając do niego poniższe:

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 = await getTasksCollection().insertOne(doc);
    res.status(201).json({ _id: result.insertedId, ...doc });
  } catch (err) {
    if (err instanceof ValidationError) {
      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 = await getTasksCollection()
      .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 = await getTasksCollection().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 = await getTasksCollection().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 instanceof ValidationError) {
      return res.status(400).json({ error: err.message });
    }
    next(err);
  }
});

// DELETE /tasks/:id
router.delete('/:id', parseObjectId, async (req, res, next) => {
  try {
    const result = await getTasksCollection().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;

Każda trasa w powyższym kodzie stosuje ten sam schemat: pobiera dane z żądania, przepuszcza je przez nasze pomocnicze funkcje, wywołuje MongoDB, a następnie zwraca odpowiedź. Dzięki gotowej walidacji i parsowaniu ID kod pozostaje krótki i przewidywalny. W praktyce wygląda to tak:

  • Trasa POST buduje nowe zadanie przy użyciu buildTaskDocument przed wstawieniem
  • Trasy GET opcjonalnie filtrują po kategorii i zwracają wyniki posortowane od najnowszych
  • Trasa PUT używa buildTaskUpdate, więc częściowe aktualizacje są bezpieczne i spójne
  • Trasa DELETE usuwa zadanie na podstawie już sparsowanego ObjectId

Warto też zwrócić uwagę na kilka innych szczegółów:

  • find() zwraca kursor, a nie tablicę, więc po sortowaniu łączymy z .toArray(), aby faktycznie otrzymać wyniki
  • findOneAndUpdate z returnDocument: 'after' od razu zwraca zaktualizowany dokument
  • Zwracamy właściwe kody HTTP: 201 dla utworzenia, 204 dla usunięcia oraz 400 lub 404 tam, gdzie to właściwe
  • Wszelkie nieoczekiwane błędy przekazujemy do next(err), aby obsłużyć je w jednym miejscu, zamiast w każdej trasie z osobna

Krok 6: Połącz wszystko w serwerze

Na tym etapie mamy już wszystko: walidację, czyste trasy i działające połączenie z bazą. Teraz musimy tylko to połączyć i faktycznie uruchomić serwer. Utwórz plik server.js. To punkt wejścia aplikacji, w którym wszystko się schodzi:

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);
}

const PORT = 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' });
});

async function start() {
  try {
    await connectDB();
    const server = app.listen(PORT, () => {
      console.log(`Server running on http://localhost:${PORT}`);
    });

    const shutdown = () => {
      console.log('\nShutting down gracefully...');
      server.close(async () => {
        await closeDB();
        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 };

Ten plik spina wszystko w czysty sposób. Sprawdza, czy ustawiono zmienne środowiskowe zanim cokolwiek ruszy, stosuje podstawowe zabezpieczenia dzięki helmet i ogranicza rozmiar przychodzącego JSON-a, aby aplikacja przypadkowo nie przyjmowała ogromnych payloadów. Serwuje też folder public/, więc frontend może działać w tej samej aplikacji, bez osobnego serwera.

Wszystkie trasy są zamontowane pod /tasks, a wszystko, co nie pasuje, zwraca przejrzyste 404. Błędy są obsługiwane w jednym miejscu, a nie powtarzane w każdej trasie, co utrzymuje spójność i ułatwia utrzymanie. Połączenie z bazą jest nawiązywane zanim serwer zacznie nasłuchiwać, więc nigdy nie przyjmujesz żądań zanim będziesz gotów, a zamykanie odbywa się elegancko, z poprawnym domykaniem połączeń.

Teraz odpalmy wszystko i sprawdźmy, czy działa, uruchamiając npm run dev. Jeśli wszystko jest połączone poprawnie, zobaczysz coś takiego:

MongoDB connected (db: taskmanager)
Server running on http://localhost:3000

W tym momencie wszystko jest spięte i działa! Mamy w pełni funkcjonalne API, więc przetestujmy je.

Krok 7: Przetestuj endpointy API

Zanim zbudujesz interfejs, warto potwierdzić, że każdy endpoint działa samodzielnie. Ułatwia to debugowanie, bo szybko sprawdzisz, czy problem leży po stronie API, czy frontendu. Użyjemy Postmana (lub dowolnego klienta HTTP) i wykonamy kilka żądań w kolejności. Każde z nich bazuje na danych utworzonych w poprzednim kroku.

Utwórz zadanie: wyślij żądanie POST z tytułem, opisem i kategorią. Jeśli wszystko działa, API zwróci 201 Created z nowym zadaniem i wygenerowanym _id.

POST http://localhost:3000/tasks
Content-Type: application/json

{
  "title": "Write GeeksForGeeks article",
  "description": "First draft by Friday",
  "category": "Work"
}

Utwórz kilka kolejnych: wykonaj to samo żądanie POST z różnymi danymi w body, żeby mieć wystarczająco dużo danych do testów listowania i filtrowania. Po tym kroku będziesz mieć mały zestaw danych do zapytań.

{ "title": "Go for a run", "category": "Personal" }
{ "title": "Read MongoDB docs", "category": "Study" }
{ "title": "Buy groceries" } // category defaults to "Other"

Wylistuj wszystkie zadania: zwróci wszystkie zadania, posortowane od najnowszych.

GET http://localhost:3000/tasks

Filtruj po kategorii: zwróci tylko zadania w kategorii Work. Indeks dodany wcześniej sprawia, że to zapytanie pozostaje szybkie wraz z rozrostem danych.

GET http://localhost:3000/tasks?category=Work

Zaktualizuj zadanie: użyj _id jednego z przed chwilą utworzonych zadań (możesz skopiować z odpowiedzi POST lub endpointu listy). Zwróci zaktualizowane zadanie. Wysyłasz tylko pola, które chcesz zmienić, więc częściowe aktualizacje są proste.

PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json

{ "completed": true }

Usuń zadanie: użyj _id zadania, które chcesz usunąć. Zwraca 204 No Content. Jeśli spróbujesz potem pobrać to samo zadanie, dostaniesz 404 z "task not found".

DELETE http://localhost:3000/tasks/<paste-task-id-here>

Na tym etapie potwierdziłeś, że wszystkie trasy działają zgodnie z oczekiwaniami. API robi swoje, więc możemy przejść do budowy interfejsu użytkownika.

Krok 8: Dodaj frontend do interakcji z API

Postman świetnie nadaje się do weryfikacji API, ale miło byłoby zobaczyć, jak działa na prawdziwej stronie. Dodajmy więc trochę frontendu do naszej aplikacji.

Zamiast wklejać tutaj duże pliki HTML i CSS, dodałem kod do tego repozytorium na GitHubie. Przejdź tam i skopiuj zawartość plików index.html, styles.css i favicon.svg do folderu public/ w katalogu głównym projektu. Zapisz zmiany i przejdź do localhost:3000. Następnie wklej poniższy kod do pliku 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');

async function api(method, path, body) {
  const res = await fetch(path, {
    method,
    headers: body ? { 'Content-Type': 'application/json' } : {},
    body: body ? JSON.stringify(body) : undefined
  });

  if (res.status === 204) return null;

  const data = await res.json();
  if (!res.ok) throw new Error(data.error || `Request failed (${res.status})`);
  return data;
}

const fetchTasks = (category) =>
  api('GET', '/tasks' + (category ? `?category=${encodeURIComponent(category)}` : ''));

const createTask = (body) => api('POST', '/tasks', body);
const updateTask = (id, body) => api('PUT', `/tasks/${id}`, body);
const deleteTask = (id) => api('DELETE', `/tasks/${id}`);

function updateCount(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`;
}

function renderTask(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;
}

function render(tasks) {
  listEl.innerHTML = '';
  emptyEl.classList.toggle('hidden', tasks.length > 0);

  for (const task of tasks) {
    listEl.appendChild(renderTask(task));
  }

  updateCount(tasks);
}

async function refresh() {
  try {
    const tasks = await fetchTasks(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 {
    await createTask({
      title,
      description: descEl.value.trim(),
      category: catEl.value
    });

    form.reset();
    await refresh();
  } catch (e) {
    errEl.textContent = e.message;
  }
});

filterEl.addEventListener('change', refresh);

refresh();

Kod JavaScript scala wszystko z powrotem z API, które zbudowałeś wcześniej. Każda funkcja odpowiada jednej z twoich tras, co utrzymuje frontend w prostocie. Wszystkie żądania przechodzą też przez pojedynczy pomocnik api(), więc obsługę błędów masz w jednym miejscu, zamiast powielać tę samą logikę wszędzie.

Filtrowanie działa poprzez przekazanie query stringu, np. ?category=Work, co bezpośrednio łączy się z logiką backendu, którą już dodałeś. Po utworzeniu, aktualizacji lub usunięciu zadania aplikacja ponownie pobiera najnowszą listę, aby UI pozostawał w synchronizacji. Licznik zadań to drobny detal, ale sprawia, że aplikacja wydaje się bardziej „żywa” w trakcie używania.

Teraz zrestartuj serwer poleceniem npm run dev, a następnie otwórz http://localhost:3000, aby wypróbować Task Manager. Dodaj kilka zadań, zaktualizuj je, przełączaj kategorie i też niektóre usuń. Na tym etapie frontend i backend są w pełni połączone i działają end-to-end.

Podsumowanie

Gratulacje! Zbudowałeś od zera kompletne API menedżera zadań z użyciem Node.js, Express i MongoDB. Zająłeś się walidacją, utrzymałeś trasy małe i spiąłeś wszystko w działający system. Co ważniejsze, pozostałeś blisko tego, jak faktycznie działa MongoDB, zamiast ukrywać je za abstrakcjami. Stąd możesz zacząć rozwijać aplikację w kierunku produkcyjnym.

Najważniejsze wnioski

  • Użycie middleware i centralizacja walidacji utrzymują kod czysty i przewidywalny.
  • Natywny sterownik MongoDB daje kontrolę bez zbędnych abstrakcji.
  • Dobrze zaprojektowane API łatwo rozbudować do aplikacji produkcyjnej.

FAQs

Czy potrzebuję Mongoose, aby zbudować API do MongoDB?

Nie. Ten samouczek używa natywnego sterownika MongoDB, który daje większą kontrolę i utrzymuje niewielki narzut. Jeśli chcesz abstrakcji schematów, możesz dodać Mongoose później.

Jak walidować dane bez schematu?

Możesz scentralizować walidację w funkcjach pomocniczych (takich jak buildTaskDocument), dzięki czemu każda trasa egzekwuje te same reguły przed zapisem do bazy.

Co się stanie, jeśli przekażę nieprawidłowy ObjectId?

Middleware odrzuca je wcześnie odpowiedzią 400, zapobiegając temu, by MongoDB zwracało mylące błędy.

Tematy
MongoDB

Najlepsze kursy DataCamp

course

Wprowadzenie do MongoDB w Pythonie

3 godz.
24.2K
Naucz się elastycznie przetwarzać i analizować dane o elastycznej strukturze za pomocą MongoDB.
Zobacz szczegółyRight Arrow
Rozpocznij Kurs
Zobacz więcejRight Arrow