Простой REST API на Node.js без фреймворка

Введение
Node.js — это открытая среда выполнения JavaScript на движке V8 от Google. Она позволяет запускать JavaScript вне браузера и хорошо подходит для серверных приложений благодаря событийной модели, большому экосистему пакетов и высокой производительности.
В этой инструкции мы построим простой CRUD API для блога на чистом Node.js (модуль http) и MongoDB с использованием mongoose. Это учебный пример: он показывает базовую структуру и логику, которые обычно подменяют фреймворки (Express, Fastify и т.д.).
Важно: пример предназначен для локальной разработки и обучения. Перед развёртыванием в продакшен примените дополнительные меры безопасности и наблюдаемости.
Что вы получите
- Минимальный сервер на http.
- Подключение к MongoDB через mongoose.
- Модель Blog с двумя полями: title и body.
- Роуты для GET/POST/PUT/DELETE.
- Чек-листы для разработчика, оператора и ревьюера.
- Советы по безопасности, тестированию и миграции на фреймворк.
Подготовка среды разработки
- Создайте директорию проекта и перейдите в неё:
mkdir nodejs-api
cd nodejs-api- Инициализируйте npm:
npm init -y- Установите mongoose (ODM для MongoDB):
npm install mongoose- Создайте файл server.js в корне проекта. Пример начальной конфигурации сервера:
const http = require("http");
const server = http.createServer((req, res) => {});
server.listen(3000, () => {
console.log("Server is running");
});Пояснение: модуль http встроен в Node.js. Метод createServer создаёт сервер и принимает callback с объектами req и res. Метод listen запускает прослушивание порта (в примере 3000).
- Создайте папки routes и models в корне проекта. В routes будет логика маршрутизации, в models — схема/модель для БД.
Подключение к базе данных
В server.js импортируйте mongoose и подключитесь к MongoDB:
const mongoose = require("mongoose");
mongoose.connect("MongoDB_URI")
.then(() => console.log('MongoDB connected'))
.catch(err => console.error('MongoDB connection error', err));Замените “MongoDB_URI” на строку подключения (например, mongodb://localhost:27017/mydb или URI из облачного провайдера). Не храните учётные данные в коде — используйте переменные окружения.
Создание модели данных
В models создайте файл blogModel.js со схемой и экспортом модели:
const mongoose = require("mongoose");
const blogSchema = mongoose.Schema({
title: {
type: String,
required: [true, "Blog must have a title"],
},
body: {
type: String,
required: [true, "Blog must have a body"],
},
});
module.exports = mongoose.model("Blog", blogSchema);Кратко: схема объявляет поля title и body как строки и делает их обязательными. Экспорт модели позволяет взаимодействовать с коллекцией через методы mongoose (find, findById, save, findByIdAndUpdate, findByIdAndDelete).
Реализация маршрутов (без фреймворка)
Без Express придётся вручную парсить URL, метод и тело запроса. Создадим routes/blogRoutes.js и экспортируем асинхронную функцию router(req, res).
const Blog = require("../models/blogModel");
const router = async function (req, res) {};
module.exports = router;Ниже — полная и понятная реализация обработчиков для основных REST-операций. Вставьте этот код внутрь router-функции в том же порядке.
GET: получить все блоги
// GET: /api/blogs
if (req.url === "/api/blogs" && req.method === "GET") {
// get all blogs
const blogs = await Blog.find();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(blogs));
}Пояснение: проверяем точный URL и метод. find() возвращает все документы.
GET: получить один блог по id
// GET: /api/blogs/:id
if (req.url.match(/\/api\/blogs\/([0-9]+)/) && req.method === "GET") {
try {
const id = req.url.split("/")[3];
const blog = await Blog.findById(id);
if (blog) {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(blog));
} else {
throw new Error("Blog does not exist");
}
} catch (error) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: error.message }));
}
}Заметка: пример использует регулярное выражение для предварительной проверки формата URL и split для извлечения id. В продакшен-решении стоит использовать более надёжную валидацию id (ObjectId для MongoDB).
POST: создать новый блог
// POST: /api/blogs
if (req.url === "/api/blogs" && req.method === "POST") {
try {
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
let blog = new Blog(JSON.parse(body));
await blog.save();
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(blog));
});
} catch (error) {
console.log(error);
}
}Пояснение: req — поток (ReadableStream). Мы собираем данные в строку, затем парсим JSON и создаём новый документ.
PUT: обновить блог по id
// PUT: /api/blogs/:id
if (req.url.match(/\/api\/blogs\/([0-9]+)/) && req.method === "PUT") {
try {
const id = req.url.split("/")[3];
let body = "";
req.on("data", (chunk) => {
body += chunk.toString();
});
req.on("end", async () => {
let updatedBlog = await Blog.findByIdAndUpdate(id, JSON.parse(body), {
new: true,
});
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify(updatedBlog));
});
} catch (error) {
console.log(error);
}
}PUT похож на POST, но извлекает id и вызывает findByIdAndUpdate.
DELETE: удалить блог по id
// DELETE: /api/blogs/:id
if (req.url.match(/\/api\/blogs\/([0-9]+)/) && req.method === "DELETE") {
try {
const id = req.url.split("/")[3];
await Blog.findByIdAndDelete(id);
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: "Blog deleted successfully" }));
} catch (error) {
res.writeHead(404, { "Content-Type": "application/json" });
res.end(JSON.stringify({ message: error.message }));
}
}Подключение маршрутизатора в server.js
Импортируйте router и передайте req, res при создании сервера:
const router = require("./routes/blogRoutes");
const server = http.createServer((req, res) => {
router(req, res);
});Теперь сервер сможет обрабатывать запросы, делегируя логику в router.
Практические замечания и когда этот подход не подходит
Важно: ручная маршрутизация без фреймворка — отличный способ понять HTTP и Node.js, но у неё есть ограничения:
- Когда нагрузка и число роутов растут, код усложняется.
- Отсутствуют удобные middleware (логирование, парсинг, обработка ошибок, CORS).
- Трудно поддерживать и тестировать без стандартных абстракций.
Используйте чистый Node.js для учебных целей, прототипов и очень лёгких микросервисов. Для реального продукта чаще выбирают Express, Fastify или NestJS.
Альтернативные подходы
- Express: минимальный, понятный API, множество middleware.
- Fastify: высокопроизводительный фреймворк с валидацией схем и плагинами.
- NestJS: структура для крупных приложений, использует декораторы и DI.
Перемиграция на фреймворк обычно требует замены ручной маршрутизации на роутеры и middleware. Для миграции подготовьте тесты и карты соответствия URL → контроллер.
Мини-методология разработки
- Сделать минимально рабочий сервер и модель.
- Добавить ручные тесты с curl/HTTPie.
- Написать автоматические тесты для каждого эндпоинта.
- Настроить логирование и обработку ошибок.
- Провести ревью безопасности перед развёртыванием.
Модель мышления и эвристики
- Разделяй ответственность: routes — маршрутизация, models — схема, server — инициализация и подключение.
- Обрабатывай все ветки ошибок: падения парсинга, ошибки БД, невалидные id.
- Не доверяй входным данным: валидируй и нормализуй.
Безопасность и жёсткое поведение в продакшене
- Не записывайте URI БД в код: используйте переменные окружения.
- Ограничьте тело запроса по размеру (защита от DoS).
- Валидация и экранирование полей (в данном примере — простые строки).
- Настройка CORS, если API доступно из браузера.
- Логи и метрики: регистрируйте ошибки и время ответа.
- Ограничение количества попыток запросов (rate limiting) на уровне прокси/веб-сервера.
Пример ограничения размера тела (вручную):
let body = "";
const MAX_SIZE = 1e6; // ~1MB
req.on('data', chunk => {
body += chunk.toString();
if (body.length > MAX_SIZE) {
res.writeHead(413, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ message: 'Payload too large' }));
req.connection.destroy();
}
});Тестирование и критерии приёмки
Критерии приёмки для базового CRUD API:
- GET /api/blogs возвращает список блогов и код 200.
- GET /api/blogs/:id для существующего id возвращает код 200 и объект.
- GET /api/blogs/:id для несуществующего id возвращает код 404.
- POST /api/blogs с валидным телом создаёт запись и возвращает код 200.
- PUT /api/blogs/:id изменяет данные и возвращает обновлённый объект.
- DELETE /api/blogs/:id удаляет запись и возвращает подтверждение.
Тесты можно реализовать с помощью mocha/chai или Jest и supertest (для фреймворков) либо с помощью прямых HTTP-запросов (node-fetch, axios).
Чек-листы по ролям
Разработчик:
- Написать и запустить unit-тесты и интеграционные тесты.
- Добавить обработку невалидного JSON.
- Проверить все ветки ошибок.
Оператор (DevOps):
- Настроить переменные окружения для строки подключения.
- Добавить мониторинг и логирование.
- Настроить ограничение ресурсов и автоскейлинг при необходимости.
Ревьювер кода:
- Проверить обработку ошибок и очистку подключений.
- Проверить отсутствие синхронных блокировок и утечек памяти.
- Убедиться в наличии входной валидации.
Миграция на фреймворк: советы
- Выделите контроллеры: каждый блок логики маршрута переместите в отдельную функцию.
- Подключите middleware для парсинга JSON и обработки ошибок.
- Перенесите валидацию в schema-валидацию (Joi, Zod) или валидацию схемы mongoose.
- Покройте всё тестами перед и после миграции.
Пример команды curl для проверки
Получить список всех блогов:
curl -X GET http://localhost:3000/api/blogsСоздать блог:
curl -X POST http://localhost:3000/api/blogs -H "Content-Type: application/json" -d '{"title":"Hello","body":"World"}'Мермайд-схема принятия решения
flowchart TD
A[Нужен быстрый прототип?] -->|Да| B'Использовать чистый Node.js'
A -->|Нет| C{Требования}
C -->|Высокая производительность| D[Fastify]
C -->|Структура и масштабируемость| E[NestJS]
C -->|Быстрая и простая настройка| F[Express]Часто задаваемые вопросы
Можно ли использовать этот код в продакшене?
Короткий ответ: можно, но с доработками. Добавьте валидацию, логирование, обработку ошибок, ограничение по размеру тела, настройте безопасность и мониторинг.
Почему в примере id обрабатывается как число в регулярном выражении?
В исходном примере регулярное выражение проверяло цифровой id. Для MongoDB чаще используются ObjectId (hex-строки). Проверьте формат id в вашей базе и адаптируйте регулярное выражение или используйте mongoose.Types.ObjectId.isValid(id).
Итог
Создать CRUD API на чистом Node.js вполне реально. Такой подход полезен для обучения и простых случаев. Для реальных проектов обычно выбирают фреймворк, который берет на себя рутинные задачи (пары методов, обработка ошибок, middleware). В любом случае правильная архитектура, тесты и меры безопасности остаются обязательными элементами зрелого процесса разработки.
Важно: храните конфиденциальные данные вне кода, валидируйте вход, логируйте и тестируйте.
Краткие выводы
- Чистый Node.js даёт глубокое понимание HTTP и потоков.
- Для продакшена пригодятся фреймворки и стандартные практики.
- Всегда выполняйте валидацию и настройте наблюдаемость.
Похожие материалы
RDP: полный гид по настройке и безопасности
Android как клавиатура и трекпад для Windows
Советы и приёмы для работы с PDF
Calibration в Lightroom Classic: как и когда использовать
Отключить Siri Suggestions на iPhone