Создайте простой менеджер задач с категориями на Node.js и MongoDB
Узнайте, как организовать задачи по категориям с помощью Node.js и MongoDB. Создайте REST API с конечными точками create, list, get, update, delete и фильтром по категории.
Обычный список дел поначалу работает отлично: вы добавляете задачи и закрываете их по мере выполнения. Но по мере роста количества задач пользоваться им становится неудобно. Одно из решений — добавить категоризацию: каждая задача будет относиться к категориям вроде Work, Study, Personal и т. п., а вы сможете фильтровать список по нужной категории.
В этом руководстве вы научитесь создавать REST API, которое позволяет организовывать задачи по категориям с помощью Node.js и MongoDB. В итоге у вас будет рабочий сервер Node.js с пятью конечными точками: create, list, get, update и delete, а также с фильтром по категории. Вы также увидите, как обеспечить проверку категорий с использованием нативного драйвера MongoDB, без ODM вроде Mongoose.
Чему вы научитесь
Как создать REST API на Node.js, Express и MongoDB, включая полноценные CRUD-эндпоинты
Как валидировать ввод, ограничивать категории и безопасно работать со значениями ObjectId
Как структурировать и тестировать API, а затем подключить его к фронтенду
Полный код этого руководства вы найдёте на GitHub, если вам удобнее клонировать репозиторий и читать по ходу.
Требования
Перед началом у вас должно быть:
Node.js 18+ (npm включён по умолчанию)
Локально запущенный MongoDB или бесплатный кластер MongoDB Atlas
Postman или любой HTTP‑клиент
Базовые знания JavaScript и концепций REST
Шаг 1. Настройка проекта
Приступим к созданию простого менеджера задач. Сначала создадим новую папку и инициализируем проект Node, выполнив команды ниже в терминале:
mkdir task-manager-categories
cd task-manager-categories
npm init -yРаботает на
Далее установим четыре рантайм-зависимости, необходимые для работы приложения: express для роутинга, официальный драйвер MongoDB, dotenv для загрузки переменных окружения и, наконец, helmet для заголовков безопасности.
npminstall express mongodb dotenv helmetРаботает на
Для разработки установите nodemon как dev‑зависимость командой ниже, чтобы сервер автоматически перезагружался при изменении файлов:
Затем откройте проект в IDE и обновите раздел scripts в package.json следующим образом. Это позволит запускать сервер с помощью npm start или в режиме разработки с npm run dev:
"scripts":{"start":"node server.js","dev":"nodemon server.js"}Работает на
Вот структура, которую мы поэтапно соберём. У каждой папки своя роль: db/ отвечает за подключение к базе, lib/ содержит вспомогательные функции, middleware/ — для middleware Express, а routes/ определяет обработчики запросов. Папка public/ будет хранить небольшой фронтенд, который мы добавим позже. Даже в маленьком проекте такая структура помогает не запутаться и сразу понимать, куда добавлять новый код. Можете создать каркас папок сейчас и заполнять по ходу или пропустить и добавлять файлы по мере выполнения шагов:
Чтобы сохранять и получать задачи, сначала подключим приложение к MongoDB. Это делается с помощью строки подключения, которая сообщает драйверу MongoDB, как соединиться с базой. Вместо того чтобы жёстко прописывать её в коде, лучше хранить её в файле окружения. Так конфиденциальные значения не попадут в репозиторий, а переключаться между окружениями станет проще.
Создайте файл .env.example для документации требуемых переменных и файл .env с вашими фактическими значениями, как показано ниже:
# 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=3000Работает на
Теперь, когда у нас есть строка подключения, используем её для настройки соединения с MongoDB. Создайте файл db/connect.js. Здесь мы инициализируем клиент MongoDB и сделаем его доступным для остальной части приложения:
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 };Работает на
Этот файл создаёт единый клиент MongoDB, который использует всё приложение. Инициализировать его нужно один раз — драйвер и так управляет пулом соединений под капотом. Создание нового клиента на каждый запрос поначалу может показаться безвредным, но очень быстро приведёт к проблемам с производительностью.
Шаг 3. Определяем структуру задачи и валидацию
На этом этапе приложение может подключаться к MongoDB. Теперь нужно определить, как выглядит задача, прежде чем что‑то сохранять.
Здесь использование нативного драйвера MongoDB ощущается иначе. Нет отдельного файла схемы, как в Mongoose. Вместо этого «схема» — это форма объекта, который вы вставляете в базу. Сначала это может показаться излишне свободным, но на деле это полезно: вы остаетесь ближе к тому, как MongoDB работает на самом деле, и ничего не скрыто за абстракциями.
Тем не менее валидация нам всё равно нужна. И вместо того чтобы размазывать логику по разным маршрутам, мы сосредоточим её в одном месте, чтобы везде действовали одинаковые правила. Создайте файл под названием lib/taskDocument.js и добавьте в него код ниже:
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
};Работает на
Этот файл — «сторож» всего, что попадает в базу данных. Каждый запрос на создание или обновление проходит через него, так что правила применяются в одном месте.
Он следит за корректной формой задач, поддерживает единообразие категорий и помогает избежать сюрпризов при запросах к данным. Пользовательский класс ValidationError также даёт аккуратный способ отделять некорректный ввод от реальных проблем сервера, чтобы API отвечало соответствующим образом. С этим на месте остальная часть приложения может оставаться простой. Каждый маршрут занимается своим делом, будучи уверенным, что получает уже валидные данные. Далее подключим маршруты и начнём сохранять задачи в MongoDB.
Шаг 4. Обработка ObjectId и валидация запросов
Теперь, когда мы знаем, как выглядит задача, разберёмся с тем, как на неё ссылаться. Когда маршрут включает параметр :id, он поступает как обычная строка. Но MongoDB ожидает ObjectId. Если строка некорректна, драйвер выбрасывает не слишком полезную ошибку. Вместо того чтобы обрабатывать это в каждом маршруте, мы централизуем логику в middleware, чтобы все конечные точки вели себя одинаково.
Для этого создайте файл middleware/parseObjectId.js и добавьте следующее:
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;Работает на
Этот middleware выполняется до обработчика маршрута. Он проверяет корректность значения id, преобразует его в ObjectId и прикрепляет к req.taskId. Так к моменту выполнения логики маршрута вы всегда работаете с корректным ObjectId, а некорректные вводимые данные отвергаются заранее с понятным ответом 400. Далее подключим это к маршрутам и начнём всё связывать вместе.
Шаг 5. Создание маршрутов задач (CRUD + фильтрация)
На этом основная работа уже сделана. Мы определили, как выглядит валидная задача, и обработали разбор и проверку ID. Значит, обработчики маршрутов могут сосредоточиться на одном — общении с базой данных.
Перейдём к созданию реальных маршрутов API: создайте файл routes/tasks.js и добавьте в него следующее:
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;Работает на
Каждый маршрут в коде выше следует одному и тому же шаблону: берёт входные данные из запроса, пропускает через созданные ранее помощники, обращается к MongoDB и возвращает ответ. Поскольку валидация и разбор ID уже реализованы, код остаётся небольшим и предсказуемым. На практике это выглядит так:
Маршрут POST формирует новую задачу с помощью buildTaskDocument перед вставкой
Маршруты GET при необходимости фильтруют по категории и возвращают результаты, отсортированные по новизне
Маршрут PUT использует buildTaskUpdate, поэтому частичные обновления безопасны и единообразны
Маршрут DELETE удаляет задачу по уже разобранному ObjectId
В этом коде также есть несколько деталей, на которые стоит обратить внимание:
find() возвращает курсор, а не массив, поэтому после сортировки мы вызываем .toArray(), чтобы получить результаты
findOneAndUpdate с опцией returnDocument: 'after' сразу возвращает обновлённый документ
Мы возвращаем корректные коды HTTP: 201 для создания, 204 для удаления и 400 или 404 по ситуации
Любые неожиданные ошибки передаются в next(err), чтобы обрабатывать их централизованно, а не в каждом маршруте
Шаг 6. Связываем всё на сервере
На этом этапе у нас есть всё необходимое: валидация, аккуратные маршруты и рабочее подключение к базе. Осталось связать всё вместе и запустить сервер. Создайте файл server.js. Это точка входа приложения, где сходятся все части:
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 };Работает на
Этот файл аккуратно связывает всё воедино. Он проверяет наличие переменных окружения до запуска, применяет базовые меры безопасности с помощью helmet и ограничивает размер входящего JSON, чтобы приложение случайно не принимало слишком большие тела запросов. Также он обслуживает папку public/, так что фронтенд может жить в том же приложении без отдельного сервера.
Все маршруты примонтированы под /tasks, а для всего, что не совпало, возвращается аккуратный 404. Ошибки обрабатываются централизованно, а не дублируются в каждом маршруте — так проще поддерживать. Подключение к базе устанавливается до старта прослушивания порта, поэтому сервер не принимает запросы, пока не готов, а завершение работы происходит корректно: соединения закрываются как следует.
Теперь запустим всё и проверим, что работает, командой npm run dev. Если всё настроено правильно, вы увидите примерно следующее:
MongoDB connected (db: taskmanager)
Server running on http://localhost:3000
Работает на
На этом всё связано и запущено! У нас есть полностью рабочее API, можно приступать к тестированию.
Шаг 7. Тестирование конечных точек API
Прежде чем строить интерфейс, полезно убедиться, что каждый эндпоинт работает сам по себе. Так отлаживать намного проще: сразу видно, в API ли проблема или во фронтенде. Используем Postman (или любой HTTP‑клиент) и выполним несколько запросов по порядку. Каждый следующий опирается на данные, созданные на предыдущем шаге.
Создайте задачу: отправьте запрос POST с заголовком, описанием и категорией. Если всё в порядке, API вернёт 201 Created с новой задачей и сгенерированным _id.
POST http://localhost:3000/tasks
Content-Type: application/json
{"title":"Write GeeksForGeeks article",
"description":"First draft by Friday",
"category":"Work"}Работает на
Создайте ещё несколько: выполните тот же запрос POST с разными телами, чтобы получить достаточно данных для тестирования списка и фильтрации. После этого у вас будет небольшой набор для запросов.
{"title":"Go for a run","category":"Personal"}{"title":"Read MongoDB docs","category":"Study"}{"title":"Buy groceries"}// category defaults to "Other"Работает на
Список всех задач: возвращает все задачи, отсортированные по новизне.
Фильтр по категории: возвращает только задачи категории Work. Индекс, добавленный ранее, обеспечивает быстроту этого запроса по мере роста данных.
GET http://localhost:3000/tasks?category=WorkРаботает на
Обновление задачи: используйте _id одной из только что созданных задач (его можно скопировать из ответа на POST или из списка). В ответ придёт обновлённая задача. Достаточно отправить только изменяемые поля — частичные обновления остаются простыми.
PUT http://localhost:3000/tasks/<paste-task-id-here>
Content-Type: application/json
{"completed":true}Работает на
Удаление задачи: используйте _id удаляемой задачи. Возвращается 204 No Content. Если попробовать снова получить эту задачу, вернётся 404 с текстом "task not found".
DELETE http://localhost:3000/tasks/<paste-task-id-here>Работает на
К этому моменту вы убедились, что все маршруты работают как ожидается. API справляется со своей задачей, значит, можно переходить к созданию интерфейса поверх него.
Шаг 8. Добавляем фронтенд для работы с API
Postman отлично подходит для проверки API, но лучше увидеть, как всё работает на реальном сайте, поэтому добавим во всё это немного фронтенда.
Вместо того чтобы вставлять сюда большие файлы HTML и CSS, код добавлен в этот репозиторий на GitHub. Перейдите туда и скопируйте содержимое файлов index.html, styles.css и favicon.svg в папку вашего проекта public/. Сохраните изменения и откройте localhost:3000. Затем вставьте следующий код в файл 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();Работает на
Здесь, в JavaScript, всё снова связывается с вашим API. Каждая функция соответствует одному из маршрутов, поэтому фронтенд легко читать. Все запросы тоже проходят через единый помощник api(), так что обработка ошибок реализована в одном месте, а не дублируется.
Фильтрация работает через строку запроса вида ?category=Work, что напрямую связывается с логикой бэкенда, которую вы уже добавили. После создания, обновления или удаления задачи приложение снова получает актуальный список, чтобы UI оставался синхронизированным. Счётчик задач — небольшая деталь, но он делает приложение более «живым» во время использования.
Теперь перезапустите сервер командой npm run dev, затем откройте http://localhost:3000 и попробуйте Task Manager. Добавляйте задачи, обновляйте их, переключайтесь между категориями и удаляйте. На этом этапе фронтенд и бэкенд полностью связаны и работают сквозным образом.
Итоги
Поздравляем! Вы с нуля создали полноценное API менеджера задач на Node.js, Express и MongoDB. Вы реализовали валидацию, сделали маршруты компактными и связали всё в рабочую систему. Что ещё важнее — вы остались ближе к реальной модели работы MongoDB, не пряча её за абстракциями. Дальше можно развивать приложение в сторону production‑готовности.
Основные выводы
Использование middleware и централизованной валидации делает код чистым и предсказуемым.
Нативный драйвер MongoDB даёт контроль без лишних абстракций.
Хорошо структурированное API легко расширить до уровня production.
FAQs
Нужен ли Mongoose для создания API с MongoDB?⌄
Нет. В этом руководстве используется нативный драйвер MongoDB, который даёт больше контроля и остаётся лёгким. Если вам нужны абстракции схем, вы можете добавить Mongoose позже.
Как валидировать данные без схемы?⌄
Вы можете централизовать валидацию во вспомогательных функциях (например, buildTaskDocument), чтобы каждый маршрут применял одинаковые правила перед записью в базу данных.
Что произойдёт, если передать некорректный ObjectId?⌄
Middleware отклонит его заранее с ответом 400, предотвращая непонятные ошибки со стороны MongoDB.
Обычный список дел поначалу работает отлично: вы добавляете задачи и закрываете их по мере выполнения. Но по мере роста количества задач пользоваться им становится неудобно. Одно из решений — добавить категоризацию: каждая задача будет относиться к категориям вроде Work, Study, Personal и т. п., а вы сможете фильтровать список по нужной категории.
В этом руководстве вы научитесь создавать REST API, которое позволяет организовывать задачи по категориям с помощью Node.js и MongoDB. В итоге у вас будет рабочий сервер Node.js с пятью конечными точками: create, list, get, update и delete, а также с фильтром по категории. Вы также увидите, как обеспечить проверку категорий с использованием нативного драйвера MongoDB, без ODM вроде Mongoose.
Чему вы научитесь
Полный код этого руководства вы найдёте на GitHub, если вам удобнее клонировать репозиторий и читать по ходу.
Требования
Перед началом у вас должно быть:
Шаг 1. Настройка проекта
Приступим к созданию простого менеджера задач. Сначала создадим новую папку и инициализируем проект Node, выполнив команды ниже в терминале:
Далее установим четыре рантайм-зависимости, необходимые для работы приложения: express для роутинга, официальный драйвер MongoDB, dotenv для загрузки переменных окружения и, наконец, helmet для заголовков безопасности.
Для разработки установите nodemon как dev‑зависимость командой ниже, чтобы сервер автоматически перезагружался при изменении файлов:
Затем откройте проект в IDE и обновите раздел scripts в package.json следующим образом. Это позволит запускать сервер с помощью npm start или в режиме разработки с npm run dev:
Вот структура, которую мы поэтапно соберём. У каждой папки своя роль: db/ отвечает за подключение к базе, lib/ содержит вспомогательные функции, middleware/ — для middleware Express, а routes/ определяет обработчики запросов. Папка public/ будет хранить небольшой фронтенд, который мы добавим позже. Даже в маленьком проекте такая структура помогает не запутаться и сразу понимать, куда добавлять новый код. Можете создать каркас папок сейчас и заполнять по ходу или пропустить и добавлять файлы по мере выполнения шагов:
Шаг 2. Подключение к MongoDB
Чтобы сохранять и получать задачи, сначала подключим приложение к MongoDB. Это делается с помощью строки подключения, которая сообщает драйверу MongoDB, как соединиться с базой. Вместо того чтобы жёстко прописывать её в коде, лучше хранить её в файле окружения. Так конфиденциальные значения не попадут в репозиторий, а переключаться между окружениями станет проще.
Создайте файл .env.example для документации требуемых переменных и файл .env с вашими фактическими значениями, как показано ниже:
Теперь, когда у нас есть строка подключения, используем её для настройки соединения с MongoDB. Создайте файл db/connect.js. Здесь мы инициализируем клиент MongoDB и сделаем его доступным для остальной части приложения:
Этот файл создаёт единый клиент MongoDB, который использует всё приложение. Инициализировать его нужно один раз — драйвер и так управляет пулом соединений под капотом. Создание нового клиента на каждый запрос поначалу может показаться безвредным, но очень быстро приведёт к проблемам с производительностью.
Шаг 3. Определяем структуру задачи и валидацию
На этом этапе приложение может подключаться к MongoDB. Теперь нужно определить, как выглядит задача, прежде чем что‑то сохранять.
Здесь использование нативного драйвера MongoDB ощущается иначе. Нет отдельного файла схемы, как в Mongoose. Вместо этого «схема» — это форма объекта, который вы вставляете в базу. Сначала это может показаться излишне свободным, но на деле это полезно: вы остаетесь ближе к тому, как MongoDB работает на самом деле, и ничего не скрыто за абстракциями.
Тем не менее валидация нам всё равно нужна. И вместо того чтобы размазывать логику по разным маршрутам, мы сосредоточим её в одном месте, чтобы везде действовали одинаковые правила. Создайте файл под названием lib/taskDocument.js и добавьте в него код ниже:
Этот файл — «сторож» всего, что попадает в базу данных. Каждый запрос на создание или обновление проходит через него, так что правила применяются в одном месте.
Он следит за корректной формой задач, поддерживает единообразие категорий и помогает избежать сюрпризов при запросах к данным. Пользовательский класс ValidationError также даёт аккуратный способ отделять некорректный ввод от реальных проблем сервера, чтобы API отвечало соответствующим образом. С этим на месте остальная часть приложения может оставаться простой. Каждый маршрут занимается своим делом, будучи уверенным, что получает уже валидные данные. Далее подключим маршруты и начнём сохранять задачи в MongoDB.
Шаг 4. Обработка ObjectId и валидация запросов
Теперь, когда мы знаем, как выглядит задача, разберёмся с тем, как на неё ссылаться. Когда маршрут включает параметр :id, он поступает как обычная строка. Но MongoDB ожидает ObjectId. Если строка некорректна, драйвер выбрасывает не слишком полезную ошибку. Вместо того чтобы обрабатывать это в каждом маршруте, мы централизуем логику в middleware, чтобы все конечные точки вели себя одинаково.
Для этого создайте файл middleware/parseObjectId.js и добавьте следующее:
Этот middleware выполняется до обработчика маршрута. Он проверяет корректность значения id, преобразует его в ObjectId и прикрепляет к req.taskId. Так к моменту выполнения логики маршрута вы всегда работаете с корректным ObjectId, а некорректные вводимые данные отвергаются заранее с понятным ответом 400. Далее подключим это к маршрутам и начнём всё связывать вместе.
Шаг 5. Создание маршрутов задач (CRUD + фильтрация)
На этом основная работа уже сделана. Мы определили, как выглядит валидная задача, и обработали разбор и проверку ID. Значит, обработчики маршрутов могут сосредоточиться на одном — общении с базой данных.
Перейдём к созданию реальных маршрутов API: создайте файл routes/tasks.js и добавьте в него следующее:
Каждый маршрут в коде выше следует одному и тому же шаблону: берёт входные данные из запроса, пропускает через созданные ранее помощники, обращается к MongoDB и возвращает ответ. Поскольку валидация и разбор ID уже реализованы, код остаётся небольшим и предсказуемым. На практике это выглядит так:
В этом коде также есть несколько деталей, на которые стоит обратить внимание:
Шаг 6. Связываем всё на сервере
На этом этапе у нас есть всё необходимое: валидация, аккуратные маршруты и рабочее подключение к базе. Осталось связать всё вместе и запустить сервер. Создайте файл server.js. Это точка входа приложения, где сходятся все части:
Этот файл аккуратно связывает всё воедино. Он проверяет наличие переменных окружения до запуска, применяет базовые меры безопасности с помощью helmet и ограничивает размер входящего JSON, чтобы приложение случайно не принимало слишком большие тела запросов. Также он обслуживает папку public/, так что фронтенд может жить в том же приложении без отдельного сервера.
Все маршруты примонтированы под /tasks, а для всего, что не совпало, возвращается аккуратный 404. Ошибки обрабатываются централизованно, а не дублируются в каждом маршруте — так проще поддерживать. Подключение к базе устанавливается до старта прослушивания порта, поэтому сервер не принимает запросы, пока не готов, а завершение работы происходит корректно: соединения закрываются как следует.
Теперь запустим всё и проверим, что работает, командой npm run dev. Если всё настроено правильно, вы увидите примерно следующее:
На этом всё связано и запущено! У нас есть полностью рабочее API, можно приступать к тестированию.
Шаг 7. Тестирование конечных точек API
Прежде чем строить интерфейс, полезно убедиться, что каждый эндпоинт работает сам по себе. Так отлаживать намного проще: сразу видно, в API ли проблема или во фронтенде. Используем Postman (или любой HTTP‑клиент) и выполним несколько запросов по порядку. Каждый следующий опирается на данные, созданные на предыдущем шаге.
Создайте задачу: отправьте запрос POST с заголовком, описанием и категорией. Если всё в порядке, API вернёт 201 Created с новой задачей и сгенерированным _id.
Создайте ещё несколько: выполните тот же запрос POST с разными телами, чтобы получить достаточно данных для тестирования списка и фильтрации. После этого у вас будет небольшой набор для запросов.
Список всех задач: возвращает все задачи, отсортированные по новизне.
Фильтр по категории: возвращает только задачи категории Work. Индекс, добавленный ранее, обеспечивает быстроту этого запроса по мере роста данных.
Обновление задачи: используйте _id одной из только что созданных задач (его можно скопировать из ответа на POST или из списка). В ответ придёт обновлённая задача. Достаточно отправить только изменяемые поля — частичные обновления остаются простыми.
Удаление задачи: используйте _id удаляемой задачи. Возвращается 204 No Content. Если попробовать снова получить эту задачу, вернётся 404 с текстом "task not found".
К этому моменту вы убедились, что все маршруты работают как ожидается. API справляется со своей задачей, значит, можно переходить к созданию интерфейса поверх него.
Шаг 8. Добавляем фронтенд для работы с API
Postman отлично подходит для проверки API, но лучше увидеть, как всё работает на реальном сайте, поэтому добавим во всё это немного фронтенда.
Вместо того чтобы вставлять сюда большие файлы HTML и CSS, код добавлен в этот репозиторий на GitHub. Перейдите туда и скопируйте содержимое файлов index.html, styles.css и favicon.svg в папку вашего проекта public/. Сохраните изменения и откройте localhost:3000. Затем вставьте следующий код в файл app.js:
Здесь, в JavaScript, всё снова связывается с вашим API. Каждая функция соответствует одному из маршрутов, поэтому фронтенд легко читать. Все запросы тоже проходят через единый помощник api(), так что обработка ошибок реализована в одном месте, а не дублируется.
Фильтрация работает через строку запроса вида ?category=Work, что напрямую связывается с логикой бэкенда, которую вы уже добавили. После создания, обновления или удаления задачи приложение снова получает актуальный список, чтобы UI оставался синхронизированным. Счётчик задач — небольшая деталь, но он делает приложение более «живым» во время использования.
Теперь перезапустите сервер командой npm run dev, затем откройте http://localhost:3000 и попробуйте Task Manager. Добавляйте задачи, обновляйте их, переключайтесь между категориями и удаляйте. На этом этапе фронтенд и бэкенд полностью связаны и работают сквозным образом.
Итоги
Поздравляем! Вы с нуля создали полноценное API менеджера задач на Node.js, Express и MongoDB. Вы реализовали валидацию, сделали маршруты компактными и связали всё в рабочую систему. Что ещё важнее — вы остались ближе к реальной модели работы MongoDB, не пряча её за абстракциями. Дальше можно развивать приложение в сторону production‑готовности.
Основные выводы