Организация приложения Node.js: файл app.js и его варианты

Раздел: Node.js -> Node.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 продолжают работать.

Файл app.js - comments

En
App js (javascript)