Статические файлы
Статические файлы — это CSS, JavaScript, изображения, шрифты и другие ресурсы, которые сервер отдаёт клиенту без обработки, напрямую из файловой системы или CDN.
Зачем нужно
Node.js-сервер может раздавать статику, но для высоконагруженных сайтов это неэффективно: сервер занят I/O вместо бизнес-логики. Правильная стратегия — отдавать статику через nginx reverse proxy или CDN, кешировать через заголовки, а Node.js использовать только для API. Понимание этого разделения критично для production-архитектуры.
Где используется
- SPA (React, Vue, Angular) —
index.html+ чанки - API-сервер — favicon, robots.txt, OpenAPI docs
- Медиафайлы загруженные пользователями (через Multer)
- Email-assets (изображения в письмах)
Основной контент
Express.static — раздача из Node.js
const express = require('express');
const path = require('path');
const app = express;
// Раздавать из папки public/
app.use(express.static(path.join(__dirname, 'public')));
// С префиксом: /static/image.png → public/image.png
app.use('/static', express.static(path.join(__dirname, 'public'), {
maxAge: '30d', // Cache-Control: max-age=2592000
immutable: true, // файлы не изменятся (для хешированных имён)
etag: true,
lastModified: true,
index: false, // не искать index.html в директории
dotfiles: 'deny' // запрет .env, .htaccess
}));
Nginx — раздача статики (production)
# nginx.conf
server {
listen 80;
server_name example.com;
# Статика — nginx обрабатывает сам
location /static/ {
alias /var/www/app/public/;
expires 30d;
add_header Cache-Control "public, immutable";
add_header Vary "Accept-Encoding";
gzip_static on; # отдавать .gz если есть
}
# Загруженные файлы
location /uploads/ {
alias /var/www/app/uploads/;
expires 7d;
}
# API — проксировать в Node.js
location /api/ {
proxy_pass http://localhost:3000;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}
Стратегия кеширования
Файлы с хешем в имени (Webpack, Vite):
main.abc123.js, style.def456.css
→ Cache-Control: max-age=31536000, immutable
→ Кешируются на год
Файлы без хеша:
index.html
→ Cache-Control: no-cache
→ Браузер проверяет ETag каждый раз
API-ответы:
→ Cache-Control: no-store
→ Не кешировать
SPA — отдача index.html для всех путей
// После API-маршрутов и статики
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
Безопасность при раздаче загруженных файлов
// Никогда не раздавать папку uploads напрямую
// app.use('/uploads', express.static('uploads')); // опасно!
// Безопасный вариант через маршрут с проверкой прав
app.get('/files/:filename', authMiddleware, async (req, res) => {
const filename = path.basename(req.params.filename); // убрать ../
const filePath = path.join(__dirname, 'uploads', filename);
// Проверить что файл принадлежит пользователю
const file = await FileService.getByFilename(filename);
if (!file || file.userId !== req.user.id) {
return res.status(403).json({ error: 'Forbidden' });
}
res.sendFile(filePath);
});
Частые ошибки
- Раздавать статику через Node.js в production — nginx/CDN справляются на порядки эффективнее
- Не устанавливать Cache-Control — браузер может не кешировать или кешировать слишком агрессивно
- Раздавать uploads без аутентификации — приватные файлы доступны всем по прямой ссылке
- Path Traversal уязвимость —
req.params.filename = '../../config.js'; всегда применятьpath.basename
Связанные темы
- _MOC Node.js
- Express -- Раздача статики
- Загрузка файлов -- Multer
- Path Traversal
- HTTP-заголовки -- основные
Ресурсы
🎓 Источник: Serving Static in Node.js
- 📅 2019-09-23 · YouTube · [Marp](../../Documents/TimurShemsedinov/2019-09-23 — Serving Static in Node.js (n_AdKIzbpBc).md)
- Тезисы:
- Статика — не задача для ноды: nginx/CDN дешевле и быстрее, но иногда уместно (SPA, малый трафик, на старте)
- Файловый указатель + индекс каталога: один Map по пути
try/catchНЕ ловит ошибку стрима — нуженstream.on('error')- Защита от path traversal: абсолютный путь +
startsWith(rootDir)обязательно - Справочник MIME types по расширению (
.js→application/javascript) - URL со слэшом в конце → отдаём
index.html ENOENTнаcreateReadStreamобрушивает процесс если не обработать- Кеш в Map в памяти — для маленьких файлов; быстрее диска в 10–100 раз
- Большие файлы (>1MB) — через стрим, не в memory
res.end(buffer)блокирует на отправку,stream.pipe(res)— нет
- Цитата: «Статика — не задача для ноды, но если уж раздаём — кэш в памяти даёт x10 к скорости»