Skip to main content

Ejemplos esenciales de backend con Node.js para desarrolladores en 2024

Escrito por

12 de junio de 2024

0 minutos de lectura

El desarrollo de backend con Node.js sigue destacándose en 2024 como un entorno de ejecución potente y flexible para crear aplicaciones escalables y eficientes, más aún con el auge de otros entornos como Bun.

En este artículo, quiero ofrecer una introducción breve a ejemplos esenciales de backend con Node.js que muestran cómo aprovechar eficazmente las funciones avanzadas de JavaScript y Node.js. Desde usar el estándar WHATWG Streams y la API Web Streams para manejar datos de forma eficiente, hasta emplear el módulo integrado `crypto` de Node.js para la seguridad, trabajar con Buffers para manipular datos binarios, aprovechar Symbols para encapsular y organizar espacios de nombres, y utilizar literales de plantilla y plantillas etiquetadas para generar HTML y consultas SQL: cada sección ofrece fragmentos de código prácticos del mundo real y consejos.

Estos ejemplos no solo muestran la versatilidad y la capacidad de Node.js para resolver desafíos de backend, sino que también son una referencia valiosa para los desarrolladores que buscan mejorar sus soluciones de backend en 2024.

Capítulos de este artículo:

  1. El estándar WHATWG Streams, la API Web Streams y los iterables asíncronos

  2. Uso del módulo Crypto para validar firmas de webhook

  3. Trabajo con buffers y datos binarios sin procesar en Node.js

  4. Uso de JavaScript Symbol para la encapsulación

  5. Literales de plantilla y plantillas etiquetadas para generar HTML y consultas SQL

Siempre puedes consultar el código fuente completo de estos ejemplos de backend con Node.js en este repositorio de GitHub.

1. El estándar WHATWG Streams, la API Web Streams y los iterables asíncronos

En 2024, el uso de streams ha aumentado significativamente, sobre todo al trabajar con modelos de lenguaje grandes para la inteligencia artificial generativa (GenAI). El SDK de OpenAI para completar chats es un excelente ejemplo de esta tendencia. Un ejemplo típico de cómo transmitir datos desde la API del SDK de OpenAI sería así:

const completion = await openai.chat.completions.create(config);

for await (const chunk of completion) {
  console.log(chunk);
}

Aquí usamos el concepto de iterables asíncronos, que ha simplificado los flujos de trabajo asíncronos en Node.js para adaptarlos al estilo de programación basado en promesas. La instrucción `for await…` crea un bucle que recorre objetos iterables asíncronos y también iterables síncronos, incluidos String, Array, objetos similares a Array (por ejemplo, arguments o NodeList), TypedArray, Map, Set e iterables asíncronos o síncronos definidos por el usuario.

Incorporación del estándar WHATWG Web Streams en Node.js

Web Streams, parte del estándar WHATWG Streams, se integró en Node.js y ofrece una forma sólida de manejar datos transmitidos en streaming. Este estándar permite a los desarrolladores leer, escribir y transformar datos de manera eficiente con JavaScript.

Veamos un ejemplo completo de cómo usar Web Streams en Node.js.

En el siguiente ejemplo, creamos una función `createReadableStreamFromFile()` que usa la clase `ReadableStream` de la API Web Streams para crear un stream de datos a partir de un archivo. Luego, una segunda función, `consumeStreamWithAsyncIterator()`, consume este stream mediante un iterador asíncrono.

import { ReadableStream } from "node:stream/web";
import fs from "fs";

function createReadableStreamFromFile(filePath) {
  const stream = new ReadableStream({
    start(controller) {
      const reader = fs.createReadStream(filePath);

      reader.on("data", (chunk) => {
        controller.enqueue(chunk);
        if (reader.readableFlowing === false) {
          reader.resume();
        }
      });

      reader.on("end", () => {
        controller.close();
      });

      reader.on("error", (err) => {
        controller.error(err);
      });
    },
  });

  return stream;
}

async function consumeStreamWithAsyncIterator(stream) {
  try {
    for await (const chunk of stream) {
      process.stdout.write(chunk);
    }
  } catch (err) {
    console.error("Error occurred while reading the stream:", err);
  }
}

const filePath = process.argv[2];
const stream = createReadableStreamFromFile(filePath);
consumeStreamWithAsyncIterator(stream);

En este código, usamos la API Web Streams para leer datos de un archivo en streaming. La función `createReadableStreamFromFile()` devuelve un objeto `ReadableStream` a partir de una ruta de archivo determinada. Luego, `consumeStreamWithAsyncIterator()` consume este stream, lee sus fragmentos uno por uno y escribe cada fragmento en la salida estándar. Si se produce un error durante la lectura, se captura y se registra en la consola.

Por cierto, ¿notaste la vulnerabilidad de seguridad en el código anterior?

Si tuvieras instalada la extensión de Snyk para VS Code, verías un error del linter subrayado que te indicaría que el ejemplo de código tiene una vulnerabilidad de recorrido de rutas.

La extensión de Snyk para VS Code te mostraría cómo los datos no seguros pueden llegar a este ejemplo de código de backend con Node.js, en qué consiste esta vulnerabilidad de seguridad y cómo corregirla con sugerencias seleccionadas por IA a partir de proyectos de código abierto activos.

La extensión de Snyk para VS Code encuentra una vulnerabilidad de seguridad en mi código de ejemplo de backend de Node.js.

2. Uso del módulo Crypto para validar firmas de webhook

Los webhooks se han convertido en una parte integral del desarrollo moderno de backend, ya que permiten que distintos servicios se comuniquen de manera eficiente y en tiempo real. Sin embargo, el mayor uso de webhooks también exige medidas de seguridad más sólidas, como la validación de las firmas de webhook.

En el contexto de los webhooks, una firma es un hash que se envía junto con la carga útil del webhook y que se calcula con una clave secreta que solo conocen el emisor y el destinatario. Luego, el destinatario puede calcular el hash por su cuenta y compararlo con la firma para verificar la autenticidad del webhook.

El módulo Crypto de Node.js ofrece numerosas funciones criptográficas, incluido un conjunto de envoltorios para las funciones de hash, HMAC, cifrado, descifrado, firma y verificación de OpenSSL. HMAC (código de autenticación de mensajes basado en hash) es especialmente útil para validar firmas de webhook.

Analicemos el fragmento de código para entender cómo funciona:

const crypto = require("node:crypto");

const secret = process.env.WEBHOOK_SECERT;
const hmac = crypto.createHmac("sha256", secret);
const digest = Buffer.from(
  hmac.update(request.rawBody).digest("hex"),
  "utf8"
);
const signature = Buffer.from(request.headers["x-signature"] || "", "utf8");

if (!crypto.timingSafeEqual(digest, signature)) {
  throw new Error("Invalid signature.");
}

En este fragmento, se usa el método `crypto.createHmac` para crear un objeto HMAC. Este método recibe dos parámetros: el algoritmo que se usará (en este caso, `sha256`) y la clave secreta.

Luego, el objeto HMAC se actualiza con el cuerpo sin procesar de la solicitud de webhook mediante el método `hmac.update`. Este método se puede llamar varias veces con datos nuevos a medida que se transmiten.

Luego, se usa el método `digest` para generar el hash. Este método solo se puede llamar una vez en el objeto HMAC y devuelve el hash calculado. El parámetro `hex` indica que el método debe devolver el hash en formato hexadecimal.

Ahora, presentamos el concepto de comparación segura de cadenas. El hash calculado (digest) y la firma recibida en el encabezado de la solicitud de webhook se comparan con el método `crypto.timingSafeEqual`. Este método compara la igualdad de dos buffers de forma segura frente a ataques de temporización, por lo que es ideal para comparar resultados criptográficos.

Los ataques de temporización son un tipo de ataque de canal lateral en el que un atacante intenta comprometer un sistema analizando el tiempo que tardan en ejecutarse los algoritmos criptográficos. Al usar un método seguro frente a ataques de temporización, como `crypto.timingSafeEqual`, nos protegemos contra este tipo de ataques. Si el digest y la firma no coinciden, se genera un error que indica que la solicitud de webhook podría no ser auténtica.

3. Trabajo con buffers y datos binarios sin procesar en Node.js

La API Buffer de Node.js es una herramienta potente que permite a los desarrolladores trabajar directamente con datos binarios. Ya sea que necesites leer un archivo, analizar una imagen o procesar datos sin procesar, la API Buffer ofrece métodos para realizar estas tareas de manera eficiente.

Exploremos algunos métodos comunes de la API Buffer en Node.js, como `.from`, `.alloc` y `.write`, que permiten crear y manipular objetos buffer. El método `.from` crea un buffer nuevo con los datos que se pasan como argumento; `.alloc` crea un buffer nuevo de un tamaño específico, y `.write` permite escribir datos en un buffer. Otro método es `.concat`, que se usa para concatenar una lista de instancias de Buffer.

let buffer1 = Buffer.from('Hello, ');
let buffer2 = Buffer.from('World!');
let buffer3 = Buffer.concat([buffer1, buffer2]);

console.log(buffer3.toString());
// prints: 'Hello, World!'

Análisis de una imagen con la API de OpenAI mediante la API Buffer de Node.js

En el siguiente ejemplo de código de backend con Node.js, usamos la API Buffer para leer un archivo de imagen y analizarlo mediante la API de OpenAI. Primero, importamos los módulos necesarios y creamos una instancia del cliente de la API de OpenAI:

import { readFile } from "node:fs/promises";
import OpenAI from "openai";

const openai = new OpenAI();

A continuación, leemos el archivo de imagen en un buffer:

const imageBuffer = await readFileToBuffer(process.argv[2]);

Luego, validamos el tipo de imagen al comparar la firma del archivo con la firma conocida de una imagen PNG:

function isImageTypeValid(imageBuffer) {
  const pngSignature = Buffer.from([
    0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a,
  ]);
  const fileSignature = imageBuffer.slice(0, 8);

  if (pngSignature.equals(fileSignature)) {
    return true;
  }
}

Por último, generamos un texto alternativo descriptivo para la imagen mediante la API de OpenAI:

async function generateAltTextForImage(imageBuffer) {
  const imageInBase64 = imageBuffer.toString("base64");

  const response = await openai.chat.completions.create({
    model: "gpt-4-vision-preview",
    messages: [
      {
        role: "user",
        content: [
          {
            type: "text",
            text: "What's in this image? generate a simple alt text for an image source in an HTML page",
          },
          {
            type: "image_url",
            image_url: {
              url: `data:image/png;base64,${imageInBase64}`,
            },
          },
        ],
      },
    ],
  });

  return response.choices[0];
}

Consideraciones de seguridad al usar la API Buffer

Al trabajar con la API Buffer, debes tener en cuenta varias consideraciones de seguridad. El manejo incorrecto de datos binarios puede provocar posibles vulnerabilidades de seguridad, como errores de desbordamiento o subdesbordamiento del buffer. Asegúrate siempre de validar las entradas y manejar los errores correctamente.

Además, ten en cuenta posibles vulnerabilidades de seguridad, como el constructor `Buffer`, que ahora está obsoleto y debe evitarse en favor de alternativas más seguras, como `Buffer.from` o `Buffer.alloc`.

4. Uso de JavaScript Symbol para la encapsulación

Los Symbols son un tipo de dato primitivo introducido en ES6 (ECMAScript 2015) que representan identificadores únicos e inmutables. Se crean con la función `Symbol()`, que puede aceptar opcionalmente una descripción (una cadena) para facilitar la depuración, aunque esta no afecta la unicidad del símbolo. Los Symbols se usan principalmente para crear claves de propiedades únicas para objetos que no entren en conflicto con ninguna otra propiedad, incluidas las heredadas. Esto los hace especialmente útiles para definir propiedades privadas o especiales de objetos sin arriesgarse a que los nombres de las propiedades entren en conflicto.

Aquí tienes un ejemplo de código de backend con Node.js que muestra cómo usar Symbols para crear una propiedad privada en una clase y encapsular datos:

const _privateProperty = Symbol('privateProperty');

class MyClass {
  constructor(value) {
    this[_privateProperty] = value;
  }

  getPrivateProperty() {
    return this[_privateProperty];
  }
}

const instance = new MyClass('secret');
console.log(instance.getPrivateProperty());
// Will output 'secret'
// The _privateProperty cannot be directly accessed from outside the class

Además, los Symbols no son accesibles mediante la enumeración de propiedades de un objeto (como los bucles `for...in` o `Object.keys()`), por lo que, en cierto sentido, pueden usarse para simular propiedades y métodos privados de objetos. Sin embargo, es importante tener en cuenta que no son realmente privados y todavía se puede acceder a ellos con métodos de reflexión como `Object.getOwnPropertySymbols()`.

JavaScript define un conjunto de Symbols conocidos que representan comportamientos internos del lenguaje que los desarrolladores pueden personalizar. Por ejemplo, puedes implementar un iterador para un objeto personalizado mediante `Symbol.iterator`:

const iterable = {
  [Symbol.iterator]: function* () {
    yield 1;
    yield 2;
  }
};

for (const value of iterable) {
  // Logs 1 and 2
  console.log(value);
}

Uso de JavaScript Symbol en Fastify

Veamos un ejemplo más cercano a un caso real con el framework de aplicaciones web Fastify y cómo el proyecto usa Symbols. En concreto, analizaremos un ejemplo de la arquitectura de plugins de Fastify. Uno de los aspectos clave del diseño de Fastify es su función de encapsulación, que permite a los desarrolladores crear contextos de aplicación aislados mediante plugins. Esto es fundamental para crear aplicaciones a gran escala, donde los conflictos entre espacios de nombres pueden convertirse en un problema.

Fastify usa Symbols para identificar de manera única las propiedades y los métodos internos, y así evitar que interfieran con las propiedades definidas por el usuario o con las de otros plugins. Aquí tienes un ejemplo simplificado, basado en el uso real de Symbols en el código fuente de Fastify, para encapsular los metadatos de un plugin:

const pluginMeta = Symbol('fastify.pluginMeta');

function registerPlugin(instance, plugin, options) {
  if (!plugin[pluginMeta]) {
    plugin[pluginMeta] = { options, name: plugin.name };
  }

  instance.register(plugin, options);
}

function myPlugin(instance, opts, done) {
  done();
}

registerPlugin(fastifyInstance, myPlugin, { prefix: '/api' });

En el ejemplo anterior, `pluginMeta` es un Symbol que Fastify usa para agregar metadatos a las funciones de los plugins. Estos metadatos incluyen las opciones y el nombre del plugin, así como otra información que el framework pueda necesitar internamente. La función `registerPlugin` simplifica el proceso de agregar metadatos a un plugin antes de registrarlo en una instancia de Fastify. Luego, estos metadatos están disponibles dentro del framework Fastify, pero permanecen aislados de la interfaz pública del plugin y del ámbito global de la aplicación.

Usar JavaScript Symbol para metadatos internos como estos ofrece varias ventajas en un framework como Fastify:

  • Encapsulación: evita que los detalles internos se filtren al espacio del usuario y mantiene la API pública limpia e intuitiva.

  • Seguridad: reduce el riesgo de interferencias accidentales entre plugins o entre un plugin y el framework principal, ya que no se puede acceder a los Symbols mediante la enumeración normal de las propiedades de un objeto.

  • Claridad: distingue claramente los mecanismos internos del framework de las API que se exponen a los desarrolladores, lo que facilita el mantenimiento y la ampliación del código de Fastify.

5. Literales de plantilla y plantillas etiquetadas para generar HTML y consultas SQL

Introducidos en ES6, los literales de plantilla ofrecen una sintaxis más legible y concisa para crear cadenas en JavaScript. Es posible que ya los hayas usado para escribir cadenas e integrar expresiones dinámicas, como `hello ${name}`. Se acabó concatenar cadenas.

Cada vez es más común usar literales de plantilla en forma de plantillas etiquetadas para generar HTML y consultas SQL en backends de Node.js. Aquí tienes dos ejemplos reales de código de backend con Node.js y SSR:

Plantillas etiquetadas para generar consultas SQL con la biblioteca PostgreSQL de Vercel

Trabajar con consultas SQL en Node.js suele generar código extenso y propenso a errores, especialmente al insertar valores dinámicamente en las consultas. El paquete PostgreSQL de Vercel (`@vercel/postgres`) ofrece una forma más segura y concisa de dar formato a las consultas SQL mediante literales de plantilla:

import { sql } from '@vercel/postgres';

const jediName = 'Luke Skywalker';
const { rows } = await sql`SELECT * FROM jedis WHERE name = ${jediName};`;

La función de literales de plantilla etiquetados con SQL interpola de forma segura la variable `jediName` en la consulta SQL, lo que previene eficazmente los ataques de inyección SQL.

Plantillas etiquetadas para generar HTML en servidores Fastify

Al igual que las consultas SQL, `fastify-html` es un plugin de Fastify que permite a los desarrolladores usar plantillas etiquetadas para generar contenido HTML directamente en los controladores de rutas. Esto puede ser especialmente útil para el renderizado del lado del servidor (SSR) o para generar contenido HTML dinámico (¿alguien dijo htmx?):

import fastify from 'fastify'
import fastifyHtml from 'fastify-html'

const app = fastify()
await app.register(fastifyHtml)

app.get('/', async (req, reply) => {
  const name = req.query.name || 'World';
  return reply.html`<h1>Hello ${name}</h1>`;
});

¿Qué son los literales de plantilla etiquetados?

Basadas en el concepto de las plantillas literales, las plantillas literales etiquetadas son una función de JavaScript que te permite definir una función personalizada (la «etiqueta») para procesar plantillas literales. Esta función recibe la plantilla literal como un arreglo de cadenas y las expresiones interpoladas como argumentos separados, lo que te permite manipular la plantilla y sus valores antes de crear la cadena final.

Para cerrar

Si te gustó este artículo, también puedes consultar las prácticas recomendadas para crear un paquete npm moderno con la seguridad en mente y 10 prácticas recomendadas para implementar aplicaciones web de Node.js en contenedores con Docker.

Y si te interesan las historias de seguridad, asegúrate de estar bien preparado para combatir los ataques a la cadena de suministro en npm con las prácticas de seguridad para desarrolladores que mencioné en el artículo.

Por último, no olvides consultar Snyk con una cuenta gratuita para empezar a proteger tu código de Node.js, tus dependencias y las imágenes de contenedor de Docker.

Leer más

Blog

Los modelos de frontera encontraron las vulnerabilidades. Solo el atacante encontró las cadenas.

El análisis estático encontró las fallas, pero solo las pruebas de ataque en vivo demostraron cómo podían encadenarse para provocar brechas. Una comparación de Evo COS, Claude Security y Claude Code Security.

feature insights context
Blog

Los ataques autónomos ya están aquí. La defensa debe estar a su altura.

Los atacantes autónomos están reduciendo el tiempo disponible para defenderse. Descubre cómo el descubrimiento, la corrección, la validación y la prevención continuos pueden ayudar a los equipos de seguridad a seguirles el ritmo.

Blog

Por qué los agentes de programación con IA siguen generando fallas de control de acceso

Los agentes de programación con IA pueden generar lógica de autorización que compila y supera la revisión, pero expone los datos de un inquilino a otro. Descubre por qué es difícil detectar el control de acceso roto y cómo prevenirlo.