Серверу постоянно нужны пути к файлам: где лежат шаблоны, куда писать
логи, какую картинку отдать по адресу /images/cat.png. Путь — это строка,
и очень хочется собирать его как строку:
const file = dir + '/' + name // так делать не надоЭто ломается чаще, чем кажется: в dir может оказаться слэш на конце
('uploads/' + '/' + 'a.png' → uploads//a.png), в имени — .., а на
Windows разделитель вообще \. Для всего этого в Node есть встроенный
модуль path:
const path = require('node:path')join: склеить куски
path.join склеивает части через правильный разделитель и сразу
нормализует результат — убирает лишние слэши, . и ..:
path.join('/app', 'uploads', 'cat.png') // '/app/uploads/cat.png'
path.join('/app/', '/uploads/', 'cat.png') // '/app/uploads/cat.png'
path.join('/app/uploads', '../logs', 'today.log') // '/app/logs/today.log'
path.join('a', '.', 'b') // 'a/b'Отдельно нормализовать готовый путь умеет path.normalize:
path.normalize('/app//src/../lib/./index.js') // '/app/lib/index.js'resolve: абсолютный путь
path.resolve тоже склеивает части, но всегда возвращает абсолютный
путь. Он идёт справа налево, пока не соберёт путь от корня, а если не
собрал — достраивает его от текущей папки процесса (process.cwd()):
// пусть process.cwd() — это '/app'
path.resolve('data', 'users.json') // '/app/data/users.json'
path.resolve('/etc', 'hosts') // '/etc/hosts'
path.resolve('/app', '/etc', 'hosts') // '/etc/hosts' — абсолютная часть «сбросила» всё слеваПоследний пример — главное отличие от join: у join все части —
просто куски одного пути, а у resolve абсолютная часть начинает путь
заново.
path.join('/app', '/etc') // '/app/etc'
path.resolve('/app', '/etc') // '/etc'Разобрать путь на части
const file = '/app/photos/2024/cat.final.jpg'
path.dirname(file) // '/app/photos/2024'
path.basename(file) // 'cat.final.jpg'
path.basename(file, '.jpg') // 'cat.final' — без расширения
path.extname(file) // '.jpg' — с точкой, только последнееВсё сразу даёт path.parse:
path.parse('/app/photos/2024/cat.final.jpg')
// {
// root: '/',
// dir: '/app/photos/2024',
// base: 'cat.final.jpg',
// name: 'cat.final',
// ext: '.jpg'
// }Обратная операция — path.format({ dir, name, ext }).
Обратите внимание на крайние случаи — именно на них ломаются «ручные»
split('.'):
path.extname('README') // '' — расширения нет
path.extname('.gitignore') // '' — это скрытый файл, а не расширение
path.extname('archive.tar.gz') // '.gz'
path.extname('/app/v1.2/readme') // '' — точка в папке не считаетсяrelative: путь от одной папки до другой
path.relative(from, to) отвечает на вопрос «как дойти от from до to»:
path.relative('/app/src', '/app/src/utils/math.js') // 'utils/math.js'
path.relative('/app/src', '/app/dist/index.js') // '../dist/index.js'
path.relative('/app/src', '/app/src') // ''Это удобно, когда нужно «перенести» файл из одной папки в другую с
сохранением вложенности — так работают сборщики: src/utils/math.ts
превращается в dist/utils/math.js.
__dirname: папка текущего файла
В каждом CommonJS-модуле есть переменная __dirname — абсолютный путь
к папке, где лежит этот файл, и __filename — путь к самому файлу.
const templates = path.join(__dirname, 'templates')Зачем это нужно? Относительные пути в fs считаются от
process.cwd() — папки, из которой запустили node, а не от папки
файла. node app/server.js и cd app && node server.js дадут разные
cwd, и fs.readFileSync('templates/index.html') сработает только в одном
случае. Путь от __dirname работает всегда.
Безопасность: выход за пределы папки
Представьте сервер, который отдаёт файлы из папки public по имени из
адреса:
const file = path.join(root, userPath) // userPath пришёл от пользователяЕсли злоумышленник попросит ../../etc/passwd, join честно нормализует
путь — и он выйдет за пределы public. Эта уязвимость называется
path traversal, и она до сих пор встречается в реальных серверах.
Защита — после сборки пути проверить, что он остался внутри корня.
Сравнивать нужно аккуратно: '/app/public-secret'.startsWith('/app/public')
— это true, хотя папка совсем другая. Проверяйте вместе с разделителем
path.sep, или через path.relative: если путь от корня до файла
начинается с .. и разделителя (или равен ..) — файл снаружи.
const rel = path.relative(root, full)
if (rel === '..' || rel.startsWith('..' + path.sep)) {
// файл вне root
}path и Windows
На Windows разделитель — \, а пути выглядят как C:\Users\anya. Модуль
path сам подстраивается под систему, поэтому код с path.join работает
везде, а код со склейкой через '/' — нет. Разделитель системы лежит в
path.sep. Если нужны именно «слэшевые» пути (например, для URL), есть
path.posix — те же функции, но всегда с /.
Первый модуль пройден
Это был последний бесплатный урок. Посмотрите, что вы уже умеете: подключать модули, читать аргументы командной строки и переменные окружения, возвращать код выхода и работать с путями через path, в том числе защищаться от выхода за пределы папки.
Дальше Node.js начинает делать настоящую работу:
- Файлы. Чтение и запись, JSON-файлы, асинхронный
fs/promises, работа с папками. - События и асинхронность.
EventEmitter, параллельное и последовательное выполнение, таймауты и повторы. - HTTP-сервер. Свой сервер, JSON API и маленький роутер в духе Express.
- Проект — API заметок. Валидация, ошибки, логи и API с хранением в файле.
Остальные модули открываются после покупки курса: один платёж, доступ навсегда. Купить можно на странице курса.