17 октября 2024 г.
Инициализация проекта
Создадим директорию и перейдите в нее:
mkdir news-api
cd news-api
Инициализируем npm проект:
npm init -y
Установим необходимые зависимости:
npm install express uuid
npm install -D nodemon
npm install express-validator
Структура проекта
- news-api/
- package.json
- server.js
- routes/
- controllers/
- data/
- middleware/
Настройка package.json
Добавим скрипты в package.json:
{
"scripts": {
"start": "node server.js",
"dev": "nodemon server.js"
}
}
Создание сервера (server.js)
const express = require('express');
const newsRoutes = require('./routes/news');
const app = express();
const PORT = process.env.PORT || 3000;
app.use(express.json());
app.use('/api/news', newsRoutes);
app.get('/', (req, res) => {
res.json({ message: 'News API is working!' });
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Something went wrong!' });
});
app.use((req, res) => {
res.status(404).json({ error: 'Route not found' });
});
app.listen(PORT, () => {
console.log(`Server is running on http://localhost:${PORT}`);
});
Модель данных (data/news.json)
Создадим файл с начальными данными:
[
{
"id": "b3251072-9185-4c0d-9279-28b315f0d047",
"title": "Новая технология в веб-разработке",
"content": "Node.js и Express продолжают развиваться, предлагая новые возможности для разработчиков.",
"category": "technology",
"author": "Eugene Starodubcev",
"published": true,
"createdAt": "2024-09-17T12:15:46.983Z",
"updatedAt": "2024-09-17T12:15:46.984Z"
}
]
Контроллер (controllers/newsController.js)
const fs = require('fs').promises;
const path = require('path');
const dataPath = path.join(__dirname, '../data/news.json');
let uuidv4;
(async () => {
const uuidModule = await import('uuid');
uuidv4 = uuidModule.v4;
})();
const readNews = async () => {
try {
const data = await fs.readFile(dataPath, 'utf8');
return JSON.parse(data);
} catch (error) {
return [];
}
};
const writeNews = async (news) => {
await fs.writeFile(dataPath, JSON.stringify(news, null, 2));
};
const getAllNews = async (req, res) => {
try {
let news = await readNews();
const { category, author, published } = req.query;
if (category) {
news = news.filter(item => item.category === category);
}
if (author) {
news = news.filter(item => item.author === author);
}
if (published !== undefined) {
const isPublished = published === 'true';
news = news.filter(item => item.published === isPublished);
}
res.json(news);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch news' });
}
};
const getNewsById = async (req, res) => {
try {
const news = await readNews();
const newsItem = news.find(item => item.id === req.params.id);
if (!newsItem) {
return res.status(404).json({ error: 'News not found' });
}
res.json(newsItem);
} catch (error) {
res.status(500).json({ error: 'Failed to fetch news' });
}
};
const createNews = async (req, res) => {
try {
const { title, content, category, author, published = false } = req.body;
if (!title || !content || !category || !author) {
return res.status(400).json({ error: 'All fields are required' });
}
const news = await readNews();
const newNewsItem = {
id: uuidv4(),
title,
content,
category,
author,
published: Boolean(published),
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
news.push(newNewsItem);
await writeNews(news);
res.status(201).json(newNewsItem);
} catch (error) {
res.status(500).json({ error: 'Failed to create news' });
}
};
const updateNews = async (req, res) => {
try {
const news = await readNews();
const newsIndex = news.findIndex(item => item.id === req.params.id);
if (newsIndex === -1) {
return res.status(404).json({ error: 'News not found' });
}
const updatedNews = {
...news[newsIndex],
...req.body,
id: news[newsIndex].id,
updatedAt: new Date().toISOString()
};
news[newsIndex] = updatedNews;
await writeNews(news);
res.json(updatedNews);
} catch (error) {
res.status(500).json({ error: 'Failed to update news' });
}
};
const deleteNews = async (req, res) => {
try {
const news = await readNews();
const newsIndex = news.findIndex(item => item.id === req.params.id);
if (newsIndex === -1) {
return res.status(404).json({ error: 'News not found' });
}
news.splice(newsIndex, 1);
await writeNews(news);
res.status(204).send();
} catch (error) {
res.status(500).json({ error: 'Failed to delete news' });
}
};
module.exports = {
getAllNews,
getNewsById,
createNews,
updateNews,
deleteNews
};
Роуты (routes/news.js)
const express = require('express');
const router = express.Router();
const {
getAllNews,
getNewsById,
createNews,
updateNews,
deleteNews
} = require('../controllers/newsController');
const {
validateCreateNews,
validateUpdateNews,
validateNewsId
} = require('../middleware/validateNews');
router.get('/', getAllNews);
router.get('/:id', validateNewsId, getNewsById);
router.post('/', validateCreateNews, createNews);
router.put('/:id', validateNewsId, validateUpdateNews, updateNews);
router.delete('/:id', validateNewsId, deleteNews);
module.exports = router;
Middleware для валидации новостей (middleware/validateNews.js)
const { body, validationResult } = require('express-validator');
const validateCreateNews = [
body('title')
.notEmpty()
.withMessage('Заголовок обязателен')
.isLength({ min: 5, max: 200 })
.withMessage('Заголовок должен быть от 5 до 200 символов')
.trim()
.escape(),
body('content')
.notEmpty()
.withMessage('Содержание обязательно')
.isLength({ min: 10 })
.withMessage('Содержание должно быть не менее 10 символов')
.trim(),
body('category')
.notEmpty()
.withMessage('Категория обязательна')
.isIn(['technology', 'politics', 'sports', 'entertainment', 'science', 'health', 'business', 'announcement'])
.withMessage('Неверная категория'),
body('author')
.notEmpty()
.withMessage('Автор обязателен')
.isLength({ min: 2, max: 50 })
.withMessage('Имя автора должно быть от 2 до 50 символов')
.trim()
.escape(),
body('published')
.optional()
.isBoolean()
.withMessage('Поле published должно быть boolean'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
});
}
next();
}
];
const validateUpdateNews = [
body('title')
.optional()
.isLength({ min: 5, max: 200 })
.withMessage('Заголовок должен быть от 5 до 200 символов')
.trim()
.escape(),
body('content')
.optional()
.isLength({ min: 10 })
.withMessage('Содержание должно быть не менее 10 символов')
.trim(),
body('category')
.optional()
.isIn(['technology', 'politics', 'sports', 'entertainment', 'science', 'health', 'business', 'announcement'])
.withMessage('Неверная категория'),
body('author')
.optional()
.isLength({ min: 2, max: 50 })
.withMessage('Имя автора должно быть от 2 до 50 символов')
.trim()
.escape(),
body('published')
.optional()
.isBoolean()
.withMessage('Поле published должно быть boolean'),
(req, res, next) => {
const errors = validationResult(req);
if (!errors.isEmpty()) {
return res.status(400).json({
error: 'Validation failed',
details: errors.array()
});
}
next();
}
];
const validateNewsId = [
(req, res, next) => {
const { id } = req.params;
if (!id || id.trim() === '') {
return res.status(400).json({ error: 'News ID is required' });
}
if (!/^[0-9a-fA-F-]{36}$/.test(id)) {
return res.status(400).json({ error: 'Invalid news ID format' });
}
next();
}
];
module.exports = {
validateCreateNews,
validateUpdateNews,
validateNewsId
};
Запуск и тестирование
Запуск в режиме разработки:
npm run dev
Или запуск в production:
npm start
Получить все новости:
curl -X GET http://localhost:3000/api/news
Получить новости с фильтрацией (по категории):
curl -X GET "http://localhost:3000/api/news?category=technology"
Получить новости с фильтрацией (по автору):
curl -X GET "http://localhost:3000/api/news?author=Admin"
Получить новости с фильтрацией (опубликованные новости):
curl -X GET "http://localhost:3000/api/news?published=true"
Получить новости с фильтрацией (комбинированные фильтры):
curl -X GET "http://localhost:3000/api/news?category=technology&published=true"
Получить конкретную новость по ID:
curl -X GET http://localhost:3000/api/news/1
Создать новую новость:
curl -X POST http://localhost:3000/api/news \
-H "Content-Type: application/json" \
-d '{
"title": "Новая технология в веб-разработке",
"content": "Node.js и Express продолжают развиваться, предлагая новые возможности для разработчиков.",
"category": "technology",
"author": "Eugene Starodubcev",
"published": true
}'
Обновить существующую новость:
curl -X PUT http://localhost:3000/api/news/1 \
-H "Content-Type: application/json" \
-d '{
"title": "Обновленный заголовок новости",
"published": false,
"content": "Обновленное содержание новости с дополнительной информацией."
}'
Удалить новость:
curl -X DELETE http://localhost:3000/api/news/1
Получить информацию о API:
curl -X GET http://localhost:3000/