Tema
fs, path y os
Tres módulos que usarás constantemente: fs para leer y escribir archivos, path para construir rutas que funcionen en cualquier sistema operativo y os para obtener información de la máquina. Con ellos podrás crear scripts de automatización, guardar logs, generar reportes y más.
Concepto
- fs (file system): es tu "explorador de archivos" programable.
- path: arma rutas correctamente. Windows usa
\y Linux/macOS usan/;pathse encarga de eso. - os: información del sistema: CPU, memoria, usuario, carpeta temporal.
fs tiene tres estilos de API:
| Estilo | Importación | Cuándo usarlo |
|---|---|---|
| Promesas | node:fs/promises | ✅ Por defecto, con async/await |
| Callbacks | node:fs | Código antiguo |
| Síncrono | node:fs (readFileSync) | Scripts sencillos o al arrancar la app |
path
js
// rutas.js (ESM)
import path from 'node:path'
const archivo = '/usuarios/ana/proyectos/reporte.final.pdf'
console.log(path.basename(archivo)) // reporte.final.pdf
console.log(path.basename(archivo, '.pdf')) // reporte.final
console.log(path.extname(archivo)) // .pdf
console.log(path.dirname(archivo)) // /usuarios/ana/proyectos
// join une segmentos con el separador correcto
console.log(path.join('datos', 'clientes', 'lista.json'))
// datos/clientes/lista.json (o datos\clientes\lista.json en Windows)
// resolve devuelve una ruta ABSOLUTA a partir de la carpeta actual
console.log(path.resolve('datos', 'lista.json'))
// C:\Users\tu\proyecto\datos\lista.json
// Ruta relativa al archivo actual (no a donde ejecutas node)
const rutaSegura = path.join(import.meta.dirname, 'datos', 'lista.json')
// Descomponer una ruta
console.log(path.parse(archivo))
// { root: '/', dir: '/usuarios/ana/proyectos', base: 'reporte.final.pdf', ext: '.pdf', name: 'reporte.final' }Error común
Usar rutas relativas como './datos.json' con fs. Son relativas a la carpeta desde donde ejecutas node (process.cwd()), no al archivo. Si ejecutas desde otra carpeta, falla. Usa path.join(import.meta.dirname, 'datos.json').
fs/promises: leer y escribir
js
// archivos.js
import { readFile, writeFile, appendFile, mkdir, readdir, stat, rm, rename, copyFile, access } from 'node:fs/promises'
import path from 'node:path'
const carpeta = path.join(import.meta.dirname, 'salida')
// Crear carpeta (recursive: no falla si ya existe y crea las intermedias)
await mkdir(carpeta, { recursive: true })
// Escribir (sobrescribe si existe)
const rutaNotas = path.join(carpeta, 'notas.txt')
await writeFile(rutaNotas, 'Primera línea\n', 'utf8')
// Agregar al final
await appendFile(rutaNotas, 'Segunda línea\n')
// Leer
const contenido = await readFile(rutaNotas, 'utf8')
console.log(contenido)
// Listar archivos de una carpeta
const archivos = await readdir(carpeta)
console.log(archivos) // [ 'notas.txt' ]
// Información de un archivo
const info = await stat(rutaNotas)
console.log(info.size, 'bytes', info.isFile(), info.mtime)
// Copiar, renombrar/mover
await copyFile(rutaNotas, path.join(carpeta, 'copia.txt'))
await rename(path.join(carpeta, 'copia.txt'), path.join(carpeta, 'respaldo.txt'))
// ¿Existe?
async function existe(ruta) {
try {
await access(ruta)
return true
} catch {
return false
}
}
console.log(await existe(rutaNotas)) // true
// Borrar archivo o carpeta completa
await rm(carpeta, { recursive: true, force: true })Manejar errores
js
import { readFile } from 'node:fs/promises'
try {
const datos = await readFile('no-existe.json', 'utf8')
} catch (error) {
if (error.code === 'ENOENT') {
console.log('El archivo no existe')
} else if (error.code === 'EACCES') {
console.log('Sin permisos')
} else {
throw error
}
}Códigos frecuentes: ENOENT (no existe), EACCES (permisos), EEXIST (ya existe), EISDIR (es una carpeta).
Trabajar con JSON
js
// db-json.js — una "base de datos" mínima en un archivo JSON
import { readFile, writeFile } from 'node:fs/promises'
import path from 'node:path'
const RUTA = path.join(import.meta.dirname, 'tareas.json')
export async function leerTareas() {
try {
return JSON.parse(await readFile(RUTA, 'utf8'))
} catch (error) {
if (error.code === 'ENOENT') return [] // primera vez
throw error
}
}
export async function guardarTareas(tareas) {
// null, 2 = formato legible con 2 espacios
await writeFile(RUTA, JSON.stringify(tareas, null, 2))
}
export async function agregarTarea(titulo) {
const tareas = await leerTareas()
const nueva = { id: crypto.randomUUID(), titulo, hecha: false }
tareas.push(nueva)
await guardarTareas(tareas)
return nueva
}Vigilar cambios
js
import { watch } from 'node:fs/promises'
const watcher = watch('./src', { recursive: true })
for await (const evento of watcher) {
console.log(`${evento.eventType}: ${evento.filename}`)
}os
js
// sistema.js
import os from 'node:os'
const gb = (bytes) => (bytes / 1024 ** 3).toFixed(2) + ' GB'
console.log('Sistema:', os.type(), os.release()) // Windows_NT 10.0...
console.log('Plataforma:', os.platform()) // win32 | linux | darwin
console.log('Arquitectura:', os.arch()) // x64 | arm64
console.log('CPU:', os.cpus()[0].model, `(${os.cpus().length} núcleos)`)
console.log('Memoria total:', gb(os.totalmem()))
console.log('Memoria libre:', gb(os.freemem()))
console.log('Usuario:', os.userInfo().username)
console.log('Carpeta personal:', os.homedir())
console.log('Temporales:', os.tmpdir())
console.log('Encendido hace:', (os.uptime() / 3600).toFixed(1), 'horas')
console.log('Salto de línea:', JSON.stringify(os.EOL))Caso real: organizador de descargas
Un script que ordena una carpeta en subcarpetas según el tipo de archivo:
js
// organizar.js
import { readdir, mkdir, rename, stat } from 'node:fs/promises'
import path from 'node:path'
import os from 'node:os'
const CATEGORIAS = {
imagenes: ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.svg'],
documentos: ['.pdf', '.docx', '.xlsx', '.pptx', '.txt', '.md'],
comprimidos: ['.zip', '.rar', '.7z'],
videos: ['.mp4', '.mov', '.mkv'],
codigo: ['.js', '.html', '.css', '.json', '.php']
}
function categoriaDe(extension) {
for (const [categoria, extensiones] of Object.entries(CATEGORIAS)) {
if (extensiones.includes(extension)) return categoria
}
return 'otros'
}
// Carpeta a organizar: argumento o Descargas por defecto
const carpeta = process.argv[2] ?? path.join(os.homedir(), 'Downloads')
const simular = process.argv.includes('--simular')
const nombres = await readdir(carpeta)
let movidos = 0
for (const nombre of nombres) {
const origen = path.join(carpeta, nombre)
const info = await stat(origen)
if (!info.isFile()) continue
const categoria = categoriaDe(path.extname(nombre).toLowerCase())
const destinoCarpeta = path.join(carpeta, categoria)
const destino = path.join(destinoCarpeta, nombre)
console.log(`${nombre} → ${categoria}/`)
if (!simular) {
await mkdir(destinoCarpeta, { recursive: true })
await rename(origen, destino)
}
movidos++
}
console.log(`\n${movidos} archivos ${simular ? 'por mover (simulación)' : 'movidos'}.`)bash
node organizar.js ./prueba --simular
node organizar.js ./pruebaBuena práctica
Agrega siempre un modo --simular (dry run) a los scripts que mueven o borran archivos. Primero ves qué pasaría y luego lo ejecutas de verdad.
Error común
Usar readFileSync dentro de una ruta de un servidor web. Cada petición bloquea el event loop. En servidores usa siempre fs/promises.
Ejercicios
- Escribe
contar.jsque reciba una carpeta y muestre cuántos archivos hay por extensión y el tamaño total en KB. - Escribe
log.jsque exporteregistrar(mensaje)y agregue líneas con fecha ISO alogs/app.log(crea la carpeta si no existe).
Ver solución
js
// contar.js
import { readdir, stat } from 'node:fs/promises'
import path from 'node:path'
const carpeta = process.argv[2] ?? '.'
const conteo = {}
let total = 0
for (const nombre of await readdir(carpeta)) {
const info = await stat(path.join(carpeta, nombre))
if (!info.isFile()) continue
const ext = path.extname(nombre).toLowerCase() || '(sin extensión)'
conteo[ext] = (conteo[ext] ?? 0) + 1
total += info.size
}
console.table(conteo)
console.log(`Tamaño total: ${(total / 1024).toFixed(1)} KB`)js
// log.js
import { appendFile, mkdir } from 'node:fs/promises'
import path from 'node:path'
const carpeta = path.join(import.meta.dirname, 'logs')
export async function registrar(mensaje) {
await mkdir(carpeta, { recursive: true })
const linea = `[${new Date().toISOString()}] ${mensaje}\n`
await appendFile(path.join(carpeta, 'app.log'), linea)
}
// Prueba rápida
await registrar('La aplicación arrancó')Resumen
- Usa
node:fs/promisesconasync/await; maneja errores porerror.code. path.joinyimport.meta.dirnamecrean rutas seguras y multiplataforma.mkdirconrecursive: trueyrmconrecursive, forcesimplifican el manejo de carpetas.osda información del sistema: memoria, CPU, carpetas del usuario.