Для перевірки роботи Ваших проектів на VDS пропонуємо Вам пільговий період, оформити замовлення на 3 дні.
Ендпоінти:
Зберігання: файл data.json (підходить для shared). Безпека: rate-limit, проста перевірка токена (опціонально). Сервісні маршрути: / health (перевірка), / (коротка підказка).
rest-api/ +-- package.json +-- app.js +-- storage.js # робота з файлом data.json +-- data.json # створиться автоматично при першому запуску +-- .env # опціонально для локального запуску
{ "name": "rest-api", "version": "1.0.0", "description": "Simple REST API (notes/todos) for shared hosting (ISPmanager).", "main": "app.js", "scripts": { "start": "node app.js", "start:prod": "NODE_ENV=production node app.js" }, "engines": { "node": ">=18.x" }, "dependencies": { "dotenv": "^16.4.5", "express": "^4.19.2", "express-rate-limit": "^7.4.0", "helmet": "^7.1.0", "morgan": "^1.10.0", "nanoid": "^5.0.7" } }
// storage.js const fs = require('fs'); const path = require('path'); const DATA_PATH = path.join(__dirname, 'data.json'); function readAll() { if (!fs.existsSync(DATA_PATH)) { fs.writeFileSync(DATA_PATH, JSON.stringify([], null, 2)); } const raw = fs.readFileSync(DATA_PATH, 'utf8'); try { return JSON.parse(raw); } catch { // якщо файл пошкоджено - перезапишемо порожнім масивом fs.writeFileSync(DATA_PATH, JSON.stringify([], null, 2)); return []; } } function writeAll(items) { fs.writeFileSync(DATA_PATH, JSON.stringify(items, null, 2)); } module.exports = { readAll, writeAll, DATA_PATH };
// app.js require('dotenv').config(); const express = require('express'); const helmet = require('helmet'); const morgan = require('morgan'); const rateLimit = require('express-rate-limit'); const { nanoid } = require('nanoid'); const { readAll, writeAll } = require('./storage'); const app = express(); const PORT = process.env.PORT || 3000; const isProd = process.env.NODE_ENV === 'production'; // БЕЗПЕКА, ЛОГИ, ПАРСИНГ app.use(helmet()); app.use(morgan(isProd ? 'combined' : 'dev')); app.use(express.json()); // API приймає JSON // ПРОСТИЙ CORS (якщо звертатиметеся з браузера) app.use((req, res, next) => { const origin = req.headers.origin || '*'; res.setHeader('Access-Control-Allow-Origin', origin); res.setHeader('Vary', 'Origin'); res.setHeader('Access-Control-Allow-Methods', 'GET,POST,DELETE,OPTIONS'); res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization'); if (req.method === 'OPTIONS') return res.sendStatus(204); next(); }); // RATE-LIMIT (анти-спам) const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 60, // 60 запитів на хвилину на IP standardHeaders: true, legacyHeaders: false }); app.use('/api/', apiLimiter); // ОПЦІОНАЛЬНИЙ ТОКЕН (задайте API_TOKEN в оточенні) // Приклад: Authorization: Bearer my-secret-token function authIfEnabled(req, res, next) { const token = process.env.API_TOKEN; if (!token) return next(); // токен не налаштований - пропускаємо const header = req.headers.authorization || ''; const provided = header.startsWith('Bearer ') ? header.slice(7) : ''; if (provided && provided === token) return next(); return res.status(401).json({ error: 'Unauthorized' }); } // INDEX/HEALTH app.get('/', (req, res) => { res.type('text/plain').send([ 'Simple REST API (notes/todos)', 'GET /api/items', 'POST /api/items { "title": "...", "done": false }', 'DELETE /api/items/:id', 'GET /health' ].join('\n')); }); app.get('/health', (req, res) => res.json({ status: 'ok', uptime: process.uptime() })); // ---- API ---- // Список app.get('/api/items', authIfEnabled, (req, res) => { const items = readAll(); res.json(items); }); // Створення app.post('/api/items', authIfEnabled, (req, res) => { const { title = '', done = false } = req.body || {}; const t = String(title).trim(); if (t.length < 1 || t.length > 200) { return res.status(400).json({ error: 'Title must be 1..200 chars' }); } const items = readAll(); const item = { id: nanoid(12), title: t, done: Boolean(done), createdAt: Date.now() }; items.unshift(item); // додамо на початок writeAll(items); res.status(201).json(item); }); // Видалення app.delete('/api/items/:id', authIfEnabled, (req, res) => { const { id } = req.params; const items = readAll(); const index = items.findIndex(i => i.id === id); if (index === -1) { return res.status(404).json({ error: 'Not found' }); } const [removed] = items.splice(index, 1); writeAll(items); res.json({ removed }); }); // 404 и 500 app.use((req, res) => res.status(404).json({ error: 'Not Found' })); app.use((err, req, res, next) => { console.error('Unhandled error:', err); res.status(500).json({ error: 'Internal Server Error' }); }); app.listen(PORT, () => { console.log(`REST API running on port ${PORT}`); });
PORT=3000 NODE_ENV=development # Опціонально – включить захист токеном: # API_TOKEN=my-secret-token
У продакшені (ISPmanager) задайте змінні в панелі. Файл .env не можна завантажувати.
npm install npm start # або npm run start:prod Перевірка: curl http://localhost:3000/health curl http://localhost:3000/api/items curl -X POST http://localhost:3000/api/items \ -H "Content-Type: application/json" \ -d '{"title":"Перша замітка","done":false}' curl -X DELETE http://localhost:3000/api/items/ Якщо увімкнули токен (API_TOKEN=...), додайте заголовок: -H "Authorization: Bearer my-secret-token"
Отримати список
curl -s https://ваш-домен/api/items
curl -s -X POST https://ваш-домен/api/items \ -H "Content-Type: application/json" \ -d '{"title":"Купити хостинг","done":false}'
curl -s -X DELETE https://ваш-домен/api/items/ID_З_СПИСКУ
Якщо увімкнено токен, додайте: -H "Authorization: Bearer my-secret-token".
Програма слухає process.env.PORT. Увімкнено HTTPS. Настроєний rate-limit. (Опціонально) Включено API_TOKEN та CORS під потрібні домени. Робите бекап data.json.