express.static это встроенный middleware, который отдаёт файлы из папки. app.use(express.static(path.join(__dirname, 'public'))) делает каждый файл из public доступным по тому же пути в URL: public/style.css отвечает на /style.css, а public/index.html отвечает на /. Express ставит Content-Type по расширению файла, а если подходящего файла нет, передаёт запрос дальше вашим маршрутам, а не отвечает 404.
Ответ на GET /style.css это текст файла, а на вкладке Headers видно Content-Type: text/css; charset=utf-8. Отправьте GET /, чтобы получить index.html, затем GET /api/time: такого файла нет, поэтому отвечает маршрут.
Зачем в пути к папке __dirname
express.static('public') ищет 'public' относительно папки, из которой вы запустили Node, а не папки, где лежит server.js. Это работает, когда вы запускаете node server.js внутри проекта, и ломается, когда приложение запускает менеджер процессов или скрипт из другого места:
// project/server.js uses express.static('public')
// cd project && node server.js -> serves project/public
// node project/server.js -> looks for ./public, every file is 404
path.join(__dirname, 'public') это папка рядом с файлом, в котором написана эта строка, откуда бы ни запустили Node. В ES-модуле используйте path.join(import.meta.dirname, 'public') (Node 20.11 и новее). Подробно __dirname и path.join разобраны на странице про модуль path.
Файлы под префиксом
Передайте первым аргументом путь подключения, чтобы отдавать папку под префиксом URL. Префикс не входит в путь к файлу на диске:
GET /assets/app.js возвращает console.log("hi"); как text/javascript, а GET /assets/img/dot.svg достаёт файл из подпапки. Отправьте GET /app.js без префикса: будет 404, потому что файлы есть только под /assets. Префикс не даёт URL файлов пересечься с маршрутами вашего API.
Заголовки кэша и maxAge
По умолчанию Express отправляет Cache-Control: public, max-age=0, поэтому браузер сверяется с сервером при каждом использовании (и получает быстрый 304 Not Modified, если файл не менялся, благодаря заголовку ETag). Для файлов, имена которых меняются при каждой сборке, например app.3f9a1c.js, разрешите браузеру их хранить:
Откройте вкладку Headers: Cache-Control теперь public, max-age=604800, то есть семь дней в секундах. index: false отключает отдачу index.html для URL папок. Другие полезные опции: extensions: ['html'], с ней /about находит about.html, и dotfiles, по умолчанию 'ignore', так что файлы вроде .env никогда не отдаются.
SPA: возврат к index.html
Приложению на React или Vue с маршрутизацией на клиенте нужно, чтобы любой неизвестный путь возвращал index.html, иначе перезагрузка на /settings даст 404. Поставьте этот запасной маршрут после express.static и маршрутов API:
app.use(express.static(path.join(__dirname, 'dist')));
app.use('/api', apiRouter);
// Express 5 wildcard syntax
app.get('/*splat', (req, res) => {
res.sendFile(path.join(__dirname, 'dist', 'index.html'));
});
Частые ошибки
Имя папки в URL. С app.use(express.static('public')) URL будет /style.css, а не /public/style.css. Если нужен /public в URL, подключите так: app.use('/public', express.static(...)).
Относительные пути к файлам в HTML. <link href="style.css"> на странице /blog/post запрашивает /blog/style.css. Начинайте путь со слэша: /style.css.
Секреты в папке со статикой. Всё, что в ней лежит, публично. Не кладите туда конфиги, ключи и исходный код.
Часто задаваемые вопросы
Можно ли использовать express.static несколько раз?
Да. Регистрируйте по одному на папку; Express ищет в них в порядке добавления и отдаёт первый найденный файл. Дайте каждому свой путь подключения, если в папках могут оказаться файлы с одинаковыми именами.
Должен ли Express раздавать статические файлы в продакшене?
Для небольших приложений это нормально. При большой нагрузке поставьте перед ним CDN или reverse proxy вроде Nginx, который отдаёт файлы быстрее и кэширует их ближе к пользователям, а Express оставьте для API.
Как отправить из маршрута один конкретный файл?
Используйте res.sendFile(path.join(__dirname, 'files', 'report.pdf')). Путь должен быть абсолютным (или передайте опцию root). Если браузер должен сохранить файл, а не показать его, используйте res.download().