Организация приложения Node.js: файл app.js и его варианты
Варианты построения файла app.js в проектах Node.js
Наиболее эффективным решением для файла app.js в современных Node.js приложениях является использование фреймворка Express с разделением ответственности на модули. Такой подход обеспечивает масштабируемость, удобство тестирования и поддержки.
Пример базовой структуры:
const express = require('express');
const app = express();
const port = process.env.PORT || 3000;
// Middleware
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Маршруты
const userRoutes = require('./routes/users');
const productRoutes = require('./routes/products');
app.use('/users', userRoutes);
app.use('/products', productRoutes);
// Обработка 404
app.use((req, res) => {
res.status(404).json({ error: 'Not Found' });
});
// Глобальный обработчик ошибок
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Internal Server Error' });
});
app.listen(port, () => {
console.log('Сервер запущен на порту ' + port);
});
App js (файл app.js)
В этом решении используется централизованное управление middleware и маршрутами. Проблемы могут возникнуть при неправильной последовательности middleware, например, если объявить обработчик 404 до маршрутов.
Типичная ошибка:
Порядок middleware имеет значение. Если поместить обработчик 404 перед маршрутами, то все запросы будут возвращать 404. Решение: располагать маршруты до обработчиков ошибок.
Как создать минимальный HTTP сервер на Node.js без фреймворков?
const http = require('http');
const server = http.createServer((req, res) => {
if (req.url === '/') {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end('Hello World');
} else {
res.writeHead(404);
res.end('Not Found');
}
});
server.listen(3000);
Этот вариант подходит для микросервисов или демонстрации. Проблемы: необходимость ручного парсинга тела запроса, управления заголовками.
Проблема:
Отсутствует встроенная поддержка JSON, форм. Для сложной логики код разрастается.
Как организовать Express приложение в одном файле app.js?
const express = require('express');
const app = express();
app.get('/', (req, res) => res.send('Hello'));
app.listen(3000);
Подходит для небольших прототипов. Недостаток: при добавлении новых маршрутов файл становится нечитаемым.
Ошибка:
Легко запутаться в импортах и middleware. Рекомендуется разделять на модули даже в малых проектах.
Как использовать Koa.js вместо Express?
const Koa = require('koa');
const app = new Koa();
app.use(async ctx => {
if (ctx.path === '/') {
ctx.body = 'Hello from Koa';
} else {
ctx.status = 404;
ctx.body = 'Not Found';
}
});
app.listen(3000);
Koa.js использует async middleware, что упрощает работу с асинхронным кодом. Однако экосистема меньше, чем у Express.
Проблема:
Некоторые middleware из Express несовместимы с Koa. Требуется адаптация.
Как разбить Express приложение на модули с роутами?
// routes/users.js
const express = require('express');
const router = express.Router();
router.get('/', (req, res) => {
res.json({ users: ['Alice', 'Bob'] });
});
module.exports = router;
// app.js
const express = require('express');
const app = express();
app.use('/users', require('./routes/users'));
app.listen(3000);
Этот вариант рекомендуется для средних и крупных проектов. Повышает читаемость и переиспользуемость кода.
Проблема:
Циклические зависимости при импорте модулей. Решение: избегать взаимных импортов, использовать dependency injection.
Как вынести настройки приложения в переменные окружения?
const dotenv = require('dotenv');
dotenv.config();
const app = require('express')();
const port = process.env.PORT || 3000;
const dbUrl = process.env.DB_URL;
app.listen(port);
Используется для разделения конфигурации разработки, тестирования и продакшена.
Ошибка:
Забыть создать файл .env или указать его в .gitignore. Решение: создать шаблон .env.example.
Как реализовать централизованную обработку ошибок в app.js?
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status || 500).json({
error: {
message: err.message || 'Внутренняя ошибка сервера'
}
});
});
// Пример маршрута с ошибкой
app.get('/error', (req, res, next) => {
next(new Error('Что-то пошло не так'));
});
Этот шаблон позволяет централизованно обрабатывать все ошибки, избегая дублирования кода.
Типичная ошибка:
Не передавать ошибку в next() корректно. В результате ошибка не попадает в обработчик.
Расширенные примеры файла app.js
Пример 1: Полное Express приложение с CORS, статикой, обработкой ошибок
const express = require('express');
const cors = require('cors');
const helmet = require('helmet');
const morgan = require('morgan');
const path = require('path');
const app = express();
// Безопасность
app.use(helmet());
app.use(cors());
// Логирование
app.use(morgan('combined'));
// Парсинг тела
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
// Статические файлы
app.use(express.static(path.join(__dirname, 'public')));
// Маршруты
app.get('/api/status', (req, res) => {
res.json({ status: 'OK', uptime: process.uptime() });
});
// Обработка 404
app.use((req, res) => {
res.status(404).sendFile(path.join(__dirname, 'public', '404.html'));
});
// Обработка ошибок
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).json({ error: 'Внутренняя ошибка сервера' });
});
const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
console.log('Сервер запущен на порту ' + PORT);
});
> Сервер запущен на порту 3000
Пояснение: helmet() защищает от распространенных веб-уязвимостей, morgan логирует входящие запросы. CORS разрешает запросы с других доменов. Статические файлы отдаются из папки public. 404 отдает кастомную HTML страницу. Глобальный обработчик ошибок возвращает JSON для API.
Пример 2: Асинхронные маршруты с обработкой ошибок
const express = require('express');
const app = express();
// Асинхронный маршрут с ошибкой
app.get('/async', async (req, res, next) => {
try {
const data = await someAsyncFunction();
res.json(data);
} catch (err) {
next(err); // передача в центральный обработчик
}
});
// Глобальный обработчик ошибок
app.use((err, req, res, next) => {
console.error(err);
res.status(500).json({ error: err.message });
});
app.listen(3000);
(при запросе /async с ошибкой)
> {"error":"Ошибка асинхронной функции"}
Пояснение: использование async/await в маршрутах требует обёртки try-catch и передачи ошибки в next(), иначе Express не обработает исключение.
Пример 3: Модульная структура с отдельными роутами и контроллерами
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.use('/users', require('./routes/users'));
app.use('/products', require('./routes/products'));
app.listen(3000);
// routes/users.js
const router = require('express').Router();
const usersController = require('../controllers/usersController');
router.get('/', usersController.getAll);
router.post('/', usersController.create);
module.exports = router;
// controllers/usersController.js
exports.getAll = (req, res) => {
res.json({ users: ['Alice'] });
};
exports.create = (req, res) => {
const { name } = req.body;
res.status(201).json({ created: name });
};
GET /users -> { "users": ["Alice"] }
POST /users with body { "name": "Bob" } -> 201 { "created": "Bob" }
Пояснение: разделение на роуты и контроллеры улучшает переиспользование и тестирование. Контроллер не зависит от роутера.
Пример 4: Использование cluster для многопроцессорности
const cluster = require('cluster');
const http = require('http');
const numCPUs = require('os').cpus().length;
if (cluster.isMaster) {
console.log('Мастер-процесс PID ' + process.pid);
for (let i = 0; i < numCPUs; i++) {
cluster.fork();
}
cluster.on('exit', (worker) => {
console.log('Воркер умер, запускаем новый');
cluster.fork();
});
} else {
http.createServer((req, res) => {
res.writeHead(200);
res.end('Привет от процесса ' + process.pid);
}).listen(3000);
console.log('Воркер запущен PID ' + process.pid);
}
Мастер-процесс PID 1234 Воркер запущен PID 1235 Воркер запущен PID 1236 (при обращении) -> Привет от процесса 1235
Пояснение: cluster позволяет распределять нагрузку между ядрами процессора. В мастер-процессе создаются воркеры. При падении воркера он автоматически перезапускается.
Пример 5: Интеграция WebSocket (socket.io) с Express
const express = require('express');
const http = require('http');
const socketIo = require('socket.io');
const app = express();
const server = http.createServer(app);
const io = socketIo(server);
app.get('/', (req, res) => {
res.sendFile(__dirname + '/index.html');
});
io.on('connection', (socket) => {
console.log('Клиент подключился');
socket.on('chat message', (msg) => {
io.emit('chat message', msg);
});
socket.on('disconnect', () => {
console.log('Клиент отключился');
});
});
server.listen(3000, () => {
console.log('Сервер с WebSocket запущен');
});
> Сервер с WebSocket запущен (при подключении клиента) -> Клиент подключился
Пояснение: Socket.IO поверх Express позволяет организовать двустороннюю связь в реальном времени. Маршруты Express продолжают работать.