Конфигурация и файлы выглядят простыми до первого production-инцидента: секрет попал в лог, запись не дождалась завершения, а дескрипторы закончились через час работы. Полезно завести строгие правила на уровне приложения.
Окружение процесса
process.env хранит строки, а не булевы значения и числа. Проверяйте и преобразуйте конфигурацию на старте, чтобы процесс упал с понятной ошибкой до открытия порта.
function positiveInt(name: string): number {
const raw = process.env[name];
const value = Number(raw);
if (!Number.isSafeInteger(value) || value <= 0) {
throw new Error(`${name} must be a positive integer`);
}
return value;
}
const port = positiveInt('PORT');
Не печатайте объект окружения целиком: там могут быть токены. process.cwd() зависит от каталога запуска, поэтому пути приложения строят от явного корня или от import.meta.url, а не от текущего каталога пользователя.
Асинхронные файлы
fs.promises не делает диск мгновенным и не запускает JavaScript в другом потоке. Операции используют механизмы libuv, а результат возвращается в основной цикл.
import { readFile } from 'node:fs/promises';
const raw = await readFile(new URL('./orders.json', import.meta.url), 'utf8');
const orders = JSON.parse(raw);
readFile загружает файл целиком. Для больших файлов и сетевых ответов выбирайте streams: иначе память растет вместе с размером файла.
Дескриптор закрывают всегда
FileHandle - ресурс ОС, а не просто объект JavaScript. Закрытие должно быть в finally, включая ошибки чтения и записи.
import { open } from 'node:fs/promises';
const file = await open('/var/lib/shop/outbox.ndjson', 'a');
try {
await file.appendFile(`${JSON.stringify({ type: 'OrderCreated', id: 42 })}\n`, 'utf8');
await file.sync();
} finally {
await file.close();
}
appendFile подтверждает запись в буфер ОС, а sync просит сбросить данные на устройство. Для outbox и журналов, после которых нельзя потерять подтвержденное событие, это разные уровни надежности.
Коротко
- В
process.envвсе строки; числа и флаги валидируют при старте. - Пути не зависят от случайного каталога запуска.
- Маленькие файлы читают целиком, большие - потоково.
FileHandleзакрывают вfinally.appendFileиsyncдают разные гарантии записи.
Что почитать дальше
- Streams и backpressure - как не загрузить память большим файлом.
- Пулы ввода-вывода - где очередь к диску и DNS.