Por qué Node.js es importante
Node.js es un runtime que ejecuta JavaScript fuera del navegador. Lanzado en 2009, tomó el motor V8 de Chrome y lo combinó con APIs para archivos, redes y procesos, permitiendo que los desarrolladores utilicen un solo lenguaje en todo el stack.
Su característica definitoria es el I/O no bloqueante y orientado a eventos. En lugar de reservar un hilo por solicitud y esperar mientras el disco o la red responden, Node delega la operación lenta y continúa con otras tareas. Cuando la operación finaliza, se ejecuta un callback. Este modelo lo hace eficiente para APIs, aplicaciones en tiempo real y herramientas, y es el mismo event loop que ya conoces de JavaScript en el navegador.
Ejecutando JavaScript en el servidor
Node te proporciona un process global, un sistema de módulos y una librería estándar. Un servidor HTTP mínimo no requiere dependencias.
// server.js
import { createServer } from "node:http";
const server = createServer((req, res) => {
if (req.url === "/health") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ status: "ok" }));
return;
}
res.writeHead(404).end("Not found");
});
server.listen(process.env.PORT ?? 3000);
Las importaciones integradas con el prefijo node: dejan claro qué módulos provienen del propio Node. Este servidor gestiona las solicitudes a medida que llegan y nunca bloquea la E/S.
Módulos
El Node moderno utiliza ES modules, la misma sintaxis de import y export que el navegador.
// math.js
export function add(a, b) {
return a + b;
}
// app.js
import { add } from "./math.js";
Configura "type": "module" en package.json para tratar los archivos .js como ES modules. Node también sigue dando soporte a CommonJS (require y module.exports), con el que te encontrarás en código antiguo y en muchos paquetes. Los proyectos nuevos deberían utilizar ES modules.
El sistema de archivos
La API del sistema de archivos de Node tiene variantes asíncronas y síncronas. Utiliza la API asíncrona en los servidores.
// files.js
import { readFile, writeFile } from "node:fs/promises";
import path from "node:path";
const file = path.join(import.meta.dirname, "data.json");
const data = JSON.parse(await readFile(file, "utf8"));
await writeFile(file, JSON.stringify({ ...data, updated: Date.now() }, null, 2));
El módulo node:path gestiona la unión de rutas entre plataformas, y import.meta.dirname proporciona el directorio del archivo actual. Existen funciones síncronas como readFileSync que son adecuadas en scripts y código de inicio, pero nunca deben usarse dentro de un manejador de solicitudes.
Entorno y configuración
La configuración debe residir en variables de entorno, no en el código.
// env.js
const config = {
port: Number(process.env.PORT ?? 3000),
databaseUrl: process.env.DATABASE_URL,
nodeEnv: process.env.NODE_ENV ?? "development",
};
Esto evita que los secretos terminen en el repositorio y permite que la misma build se ejecute en desarrollo, staging y producción. En desarrollo, carga los valores locales desde un archivo .env utilizando una herramienta como dotenv o la funcionalidad integrada --env-file de Node.js, y nunca subas ese archivo al repositorio.
Creación de APIs
La mayoría de las aplicaciones de Node.js utilizan un framework para gestionar el routing, el parsing y el middleware. Express es el más utilizado; Fastify es más rápido y está orientado a esquemas.
// api.js
import express from "express";
const app = express();
app.use(express.json());
app.get("/api/users", async (req, res, next) => {
try {
const users = await db.users.findMany();
res.json(users);
} catch (error) {
next(error);
}
});
app.post("/api/users", async (req, res, next) => {
try {
const user = await db.users.create(req.body);
res.status(201).json(user);
} catch (error) {
next(error);
}
});
app.use((error, req, res, next) => {
console.error(error);
res.status(500).json({ message: "Something went wrong" });
});
app.listen(config.port);
Los manejadores de rutas (route handlers) son funciones async, los errores se redirigen al middleware y un manejador de errores final convierte las excepciones en respuestas. Devuelve códigos de estado y encabezados significativos, y consulta Seguridad Web para validar la entrada y proteger las sesiones.
Streams, buffers y eventos
Node maneja los datos como streams para que las cargas pesadas no tengan que caber enteramente en la memoria.
// stream.js
import { createReadStream } from "node:fs";
import { createServer } from "node:http";
createServer((req, res) => {
createReadStream("movie.mp4").pipe(res);
}).listen(3000);
Un buffer son datos binarios en bruto, y un event emitter permite que los objetos publiquen eventos a los que otros se suscriben. Los streams y los eventos son la maquinaria de bajo nivel detrás de gran parte de la librería estándar de Node; entenderlos permite comprender cómo funcionan los archivos, los sockets y HTTP internamente.
Errores y señales de proceso
Los errores no controlados pueden provocar la caída de un proceso de Node.js, por lo que debes gestionarlos de manera deliberada.
// signals.js
process.on("SIGTERM", () => {
server.close(() => process.exit(0));
});
process.on("unhandledRejection", (reason) => {
console.error("Unhandled rejection", reason);
});
SIGTERM es enviada por la mayoría de las plataformas cuando un contenedor o proceso debe detenerse; gestionarla te permite finalizar las solicitudes en curso antes de salir. Los manejadores globales son una red de seguridad para el registro de logs, no un sustituto de la gestión de errores en el lugar donde ocurren.
Mejores prácticas
- Utiliza APIs async en los request handlers y nunca bloquees el event loop.
- Mantén la configuración en variables de entorno.
- Valida y sanitiza todas las entradas en el servidor.
- Utiliza ES modules y el prefijo
node:para los built-ins. - Centraliza el manejo de errores en middleware.
- Transmite archivos grandes mediante streams en lugar de leerlos en memoria.
- Gestiona
SIGTERMpara un apagado controlado (graceful shutdown) y expón un endpoint de salud (health endpoint).
Errores comunes
- Usar
readFileSyncdentro de un request handler y bloquear el servidor. - Realizar tareas pesadas de CPU en el hilo principal.
- Hacer commit de archivos
.envo escribir secretos directamente en el código. - Confiar en la entrada del cliente sin validarla.
- Ignorar las promesas rechazadas no gestionadas (unhandled promise rejections).
- Leer archivos grandes completamente en memoria en lugar de usar streaming.
Próximos pasos
Node.js completa el panorama full-stack. Gestiona las dependencias con npm, comprende el protocolo HTTP que utiliza tu servidor, asegúralo con Web Security y empaquétalo con Docker. Después, construye una API pequeña con dos rutas, una llamada a la base de datos y un manejador de errores, y despliégala.