Kurs
Eine einfache To-do-Liste ist am Anfang super: Du fügst ein paar Aufgaben hinzu und hakst sie ab, wenn sie erledigt sind. Doch je mehr Aufgaben dazukommen, desto mühsamer wird die Nutzung. Eine Sache, die hilft: Kategorien einführen. So kann jede Aufgabe Bereichen wie Work, Study, Personal usw. zugeordnet werden – und du filterst die Liste einfach nach der passenden Kategorie.
In diesem Tutorial lernst du, wie du eine REST-API baust, mit der du Aufgaben per Kategorie organisierst – mit Node.js und MongoDB. Am Ende hast du einen laufenden Node.js-Server mit fünf Endpunkten: create, list, get, update und delete, plus Kategoriefilter. Außerdem siehst du, wie du die Kategorienvalidierung mit dem nativen MongoDB-Treiber durchsetzt – ganz ohne ein ODM wie Mongoose.
Was du lernen wirst
- Wie du mit Node.js, Express und MongoDB eine REST-API inklusive kompletter CRUD-Endpunkte aufbaust
- Wie du Eingaben validierst, Kategorien erzwingst und ObjectId-Werte sicher handhabst
- Wie du deine API strukturierst, testest und anschließend mit einem Frontend verbindest
Den kompletten Code zu diesem Tutorial findest du auf GitHub – wenn du möchtest, klonst du ihn und liest parallel mit.
Voraussetzungen
Bevor du startest, solltest du Folgendes haben:
- Node.js 18+ installiert (npm ist standardmäßig enthalten)
- MongoDB lokal laufend oder ein kostenloser MongoDB Atlas-Cluster
- Postman oder ein beliebiger HTTP-Client
- Grundkenntnisse in JavaScript und REST-Konzepten
Schritt 1: Projekt einrichten
Legen wir los und bauen unseren einfachen Task-Manager. Als Erstes erstellen wir einen neuen Ordner und initialisieren ein Node-Projekt, indem wir die folgenden Befehle im Terminal ausführen:
mkdir task-manager-categories
cd task-manager-categories
npm init -y
Als Nächstes installieren wir vier Laufzeitabhängigkeiten, die unsere App benötigt: express für Routing, den offiziellen MongoDB-Treiber, dotenv zum Laden von Umgebungsvariablen und schließlich helmet für Security-Header.
npm install express mongodb dotenv helmet
Für die Entwicklung installierst du nodemon als Dev-Dependency mit dem folgenden Befehl, damit der Server bei Dateiänderungen automatisch neu lädt:
npm install --save-dev nodemon
Öffne anschließend den Projektordner in deiner IDE und aktualisiere den scripts-Abschnitt in der package.json wie folgt. So kannst du den Server mit npm start starten oder im Entwicklungsmodus mit npm run dev laufen lassen:
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
Hier ist die Projektstruktur, die wir schrittweise aufbauen. Jeder Ordner hat eine klare Aufgabe: db/ kümmert sich um die Datenbankverbindung, lib/ enthält Helferfunktionen, middleware/ ist für Express-Middleware und routes/ definiert die Request-Handler. Der Ordner public/ hält ein kleines Frontend, das wir später hinzufügen. Selbst bei einem kleinen Projekt sorgt diese Struktur für Übersichtlichkeit und macht klar, wo neuer Code hingehört. Du kannst das Gerüst jetzt anlegen und nach und nach füllen – oder die Dateien einfach in den nächsten Schritten hinzufügen:
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
Schritt 2: Verbindung zu MongoDB herstellen
Um Aufgaben zu speichern und abzurufen, müssen wir unsere App mit MongoDB verbinden. Das geschieht über einen Connection-String, der dem MongoDB-Treiber sagt, wie er deine Datenbank erreicht. Anstatt ihn hart im Code zu hinterlegen, ist es besser, ihn in einer Umgebungsdatei zu verwalten. So bleiben sensible Werte außerhalb des Codes, und der Wechsel zwischen Umgebungen wird einfacher.
Erstelle eine Datei .env.example, um die benötigten Variablen zu dokumentieren, und eine Datei .env mit deinen echten Werten – so etwa:
# 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
Jetzt, da wir einen Connection-String haben, können wir die MongoDB-Verbindung einrichten. Lege die Datei db/connect.js an. Hier initialisieren wir den MongoDB-Client und machen ihn für den Rest der App verfügbar:
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 };
Diese Datei richtet einen einzigen MongoDB-Client ein, den der Rest der App wiederverwendet. Du solltest ihn nur einmal erstellen, denn der Treiber übernimmt das Connection-Pooling intern. Für jede Anfrage einen neuen Client zu starten, wirkt anfangs harmlos, führt aber schnell zu Performance-Problemen.
Schritt 3: Aufgabenstruktur und Validierung definieren
Die App kann sich jetzt mit MongoDB verbinden. Bevor wir etwas speichern, müssen wir festlegen, wie eine Aufgabe eigentlich aussieht.
Hier fühlt sich die Arbeit mit dem nativen MongoDB-Treiber etwas anders an. Es gibt keine Schema-Datei wie bei Mongoose. Stattdessen ist das „Schema“ einfach die Form des Objekts, das du in die Datenbank einfügst. Das wirkt zunächst locker, ist aber hilfreich, weil du näher an dem bleibst, was MongoDB wirklich tut – ohne unnötige Abstraktionen.
Trotzdem wollen wir Validierung. Anstatt die Logik über verschiedene Routen zu verstreuen, bündeln wir sie an einer Stelle, damit überall die gleichen Regeln gelten. Lege die Datei lib/taskDocument.js an und füge folgenden Code ein:
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
};
Diese Datei ist das Nadelöhr für alles, was in deine Datenbank geht. Jede Create- oder Update-Anfrage läuft hier durch – so stellst du sicher, dass überall dieselben Regeln gelten.
Sie sorgt dafür, dass Aufgaben immer die richtige Form haben, hält Kategorien konsistent und verhindert böse Überraschungen beim Abfragen. Die eigene ValidationError-Klasse trennt außerdem ungültige Eingaben sauber von echten Serverproblemen – so kann deine API passend reagieren. Mit dieser Grundlage bleibt der Rest der App einfach. Jede Route kann sich auf ihre Aufgabe konzentrieren, weil die Daten bereits gültig sind. Als Nächstes verdrahten wir die Routen und speichern Aufgaben in MongoDB.
Schritt 4: ObjectId und Request-Validierung behandeln
Jetzt, da wir wissen, wie eine Aufgabe aussieht, kümmern wir uns um ihre Referenz. Immer wenn eine Route einen :id-Parameter enthält, kommt dieser als String an. MongoDB erwartet jedoch eine ObjectId. Ist der String fehlerhaft, wirft der Treiber eine wenig hilfreiche Fehlermeldung. Anstatt das in jeder Route zu behandeln, zentralisieren wir es als Middleware – so verhalten sich alle Endpunkte einheitlich.
Erstelle dazu die Datei middleware/parseObjectId.js und füge Folgendes ein:
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;
Diese Middleware läuft vor deinem Route-Handler. Sie prüft die Gültigkeit der id, wandelt sie in eine ObjectId um und hängt sie an req.taskId. Wenn deine Routenlogik greift, arbeitest du also immer mit einer gültigen ObjectId – und ungültige Eingaben werden früh mit einem klaren 400 abgelehnt. Als Nächstes binden wir das in unsere Routen ein und verdrahten alles.
Schritt 5: Task-Routen bauen (CRUD + Filter)
Der Großteil der Vorarbeit ist erledigt. Wir wissen, wie eine gültige Aufgabe aussieht, und wir haben das Parsen und Validieren von IDs geklärt. Dadurch können sich die Route-Handler auf das Wesentliche konzentrieren: den Datenbankzugriff.
Lass uns die eigentlichen API-Routen bauen. Erstelle die Datei routes/tasks.js und füge Folgendes ein:
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;
Jede Route folgt demselben Muster: Eingaben aus dem Request holen, durch unsere Helfer jagen, MongoDB aufrufen und eine Antwort zurückgeben. Da Validierung und ID-Parsen bereits geklärt sind, bleibt der Code schlank und vorhersehbar. In der Praxis sieht das so aus:
- Die POST-Route baut mit buildTaskDocument eine neue Aufgabe, bevor sie eingefügt wird
- Die GET-Routen filtern optional nach Kategorie und sortieren neueste zuerst
- Die PUT-Route nutzt buildTaskUpdate – Teilupdates sind damit sicher und konsistent
- Die DELETE-Route löscht per bereits geparster ObjectId
Ein paar weitere Details sind ebenfalls nennenswert:
- find() liefert einen Cursor, kein Array – daher hängen wir nach dem Sortieren .toArray() an, um die Ergebnisse zu bekommen
- findOneAndUpdate mit returnDocument: 'after' liefert dir direkt das aktualisierte Dokument
- Wir geben sinnvolle HTTP-Statuscodes zurück: 201 für Create, 204 für Delete sowie 400 bzw. 404 wo angebracht
- Unerwartete Fehler gehen an next(err) – so werden sie zentral statt in jeder Route behandelt
Schritt 6: Alles im Server zusammenführen
Jetzt sind alle Bausteine vorhanden: Validierung, aufgeräumte Routen und eine funktionierende Datenbankverbindung. Wir müssen nur noch alles verdrahten und den Server starten. Erstelle die Datei server.js. Das ist der Einstiegspunkt der App, an dem alles zusammenläuft:
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 };
Diese Datei führt alles sauber zusammen. Sie prüft vor dem Start, ob die Umgebungsvariablen gesetzt sind, wendet mit helmet Basisschutz an und begrenzt eingehendes JSON, damit nicht versehentlich riesige Payloads akzeptiert werden. Außerdem wird der Ordner public/ bedient, sodass dein Frontend im selben Projekt leben kann – ohne separaten Server.
Alle Routen sind unter /tasks gemountet; alles andere liefert sauber 404. Fehler werden zentral behandelt statt in jeder Route – das macht es konsistent und wartungsfreundlich. Die Datenbankverbindung steht, bevor der Server lauscht, sodass du keine Anfragen annimmst, bevor alles bereit ist. Das Herunterfahren erfolgt sauber, Verbindungen werden ordentlich geschlossen.
Starte jetzt mit npm run dev. Wenn alles korrekt verdrahtet ist, solltest du etwa Folgendes sehen:
MongoDB connected (db: taskmanager)
Server running on http://localhost:3000
Damit läuft alles! Wir haben eine voll funktionsfähige API – testen wir sie.
Schritt 7: Die API-Endpunkte testen
Bevor wir ein UI bauen, prüfen wir, ob jeder Endpunkt für sich funktioniert. Das vereinfacht das Debugging, weil du schnell siehst, ob ein Problem aus der API oder dem Frontend kommt. Wir nutzen Postman (oder einen beliebigen HTTP-Client) und führen einige Requests der Reihe nach aus. Jeder baut auf den Daten des vorherigen Schritts auf.
Aufgabe erstellen: Sende eine POST-Anfrage mit Titel, Beschreibung und Kategorie. Wenn alles klappt, antwortet die API mit 201 Created, der neuen Aufgabe und der generierten _id.
POST http://localhost:3000/tasks
Content-Type: application/json
{
"title": "Write GeeksForGeeks article",
"description": "First draft by Friday",
"category": "Work"
}
Ein paar weitere erstellen: Führe denselben POST-Request mit unterschiedlichen Bodys aus, damit du genug Daten zum Listen und Filtern hast. Danach hast du ein kleines Dataset zum Abfragen.
{ "title": "Go for a run", "category": "Personal" }
{ "title": "Read MongoDB docs", "category": "Study" }
{ "title": "Buy groceries" } // category defaults to "Other"
Alle Aufgaben auflisten: Liefert alle Aufgaben, nach neuesten zuerst sortiert.
GET http://localhost:3000/tasks
Nach Kategorie filtern: Liefert nur die Work-Aufgaben. Der zuvor angelegte Index hält diese Abfrage auch bei wachsendem Datenvolumen schnell.
GET http://localhost:3000/tasks?category=Work
Aufgabe aktualisieren: Nutze die _id einer der gerade erstellten Aufgaben (du kannst sie aus der POST-Antwort oder dem List-Endpunkt kopieren). Es kommt die aktualisierte Aufgabe zurück. Du sendest nur die Felder, die sich ändern sollen – Teilupdates bleiben also simpel.
PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json
{ "completed": true }
Aufgabe löschen: Nutze die _id der zu löschenden Aufgabe. Die Antwort ist 204 No Content. Versuchst du anschließend, dieselbe Aufgabe abzurufen, bekommst du 404 mit "task not found".
DELETE http://localhost:3000/tasks/<paste-task-id-here>
Damit hast du bestätigt, dass alle Routen wie erwartet funktionieren. Die API macht ihren Job – jetzt bauen wir ein UI oben drauf.
Schritt 8: Ein Frontend hinzufügen, das mit der API interagiert
Postman ist super zum Verifizieren der API – aber noch besser ist es, das Ganze auf einer Website zu sehen. Fügen wir also etwas Frontend-Code zu unserer Anwendung hinzu.
Statt große HTML- und CSS-Dateien hier einzufügen, habe ich den Code in dieses GitHub-Repo gepackt. Hol dir dort die Inhalte von index.html, styles.css und favicon.svg in deinen Ordner public/ im Projektroot. Speichere die Änderungen und gehe zu localhost:3000. Dann füge den folgenden Code in die Datei app.js ein:
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();
Das JavaScript verbindet alles zurück zu deiner API. Jede Funktion mappt auf eine deiner Routen – so bleibt das Frontend leicht nachvollziehbar. Alle Requests laufen zudem durch einen gemeinsamen api()-Helper, sodass du Fehlerbehandlung nicht überall wiederholen musst.
Das Filtern funktioniert über einen Query-String wie ?category=Work – das greift direkt die Backend-Logik auf, die du bereits eingebaut hast. Nach dem Erstellen, Aktualisieren oder Löschen holt die App die aktuelle Liste erneut, damit die UI synchron bleibt. Der kleine Task-Zähler ist nur ein Detail, macht die App aber lebendiger in der Nutzung.
Starte den Server jetzt neu mit npm run dev und öffne http://localhost:3000, um den Task-Manager auszuprobieren. Füge Aufgaben hinzu, aktualisiere sie, wechsle zwischen Kategorien und lösche auch mal welche. Damit sind Frontend und Backend Ende-zu-Ende verbunden.
Fazit
Glückwunsch! Du hast eine komplette Task-Manager-API von Grund auf mit Node.js, Express und MongoDB gebaut. Du hast validiert, deine Routen schlank gehalten und alles zu einem funktionierenden System verdrahtet. Vor allem bist du nah an der Arbeitsweise von MongoDB geblieben – ohne sie hinter Abstraktionen zu verstecken. Von hier aus kannst du die App Richtung Produktion weiterentwickeln.
Wichtigste Erkenntnisse
- Middleware und zentrale Validierung halten deinen Code sauber und gut vorhersagbar.
- Der native MongoDB-Treiber gibt dir Kontrolle ohne unnötige Abstraktion.
- Eine gut strukturierte API lässt sich leicht zu einer produktionsreifen App ausbauen.
FAQs
Brauche ich Mongoose, um eine MongoDB-API zu bauen?
Nein. Dieses Tutorial verwendet den nativen MongoDB-Treiber – das gibt dir mehr Kontrolle und hält alles leichtgewichtig. Wenn du Schema-Abstraktionen möchtest, kannst du Mongoose später jederzeit ergänzen.
Wie valide ich Daten ohne Schema?
Du kannst die Validierung in Helferfunktionen zentralisieren (z. B. in buildTaskDocument), sodass jede Route vor dem Schreiben in die Datenbank dieselben Regeln durchsetzt.
Was passiert, wenn ich eine ungültige ObjectId übergebe?
Die Middleware lehnt sie früh mit einer 400-Antwort ab und verhindert so verwirrende Fehler von MongoDB.