Node.js

Node

Tutorial Node: Módulo http y https

Node.js: Aprende a crear un servidor básico usando http.createServer(). Comprende solicitudes y respuestas HTTP de forma sencilla y eficiente.

Aprende Node GRATIS y certifícate

Creando un servidor básico con http.createServer()

El módulo http de Node.js permite la creación de servidores web y el manejo de solicitudes HTTP de forma sencilla. Para iniciar un servidor básico, utilizamos el método **http.createServer()**, que devuelve una instancia de servidor.

Primero, importamos el módulo http:

import { createServer } from "http";

A continuación, creamos el servidor:

const servidor = createServer((req, res) => {
    // Manejar la solicitud y enviar una respuesta
});

La función de callback recibe dos objetos importantes: req (la solicitud entrante) y res (la respuesta que enviaremos al cliente). Estos objetos nos permiten acceder a información de la solicitud y configurar la respuesta respectivamente.

Dentro del callback, podemos establecer el código de estado HTTP y los encabezados de la respuesta:

res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');

Aquí, hemos establecido un código de estado 200, indicando éxito, y especificado el tipo de contenido como texto plano en UTF-8. Luego, enviamos la respuesta al cliente:

res.end('¡Hola, mundo!');

El método res.end() finaliza la respuesta y envía los datos al cliente. Si necesitamos enviar datos en múltiples partes, podríamos usar res.write() antes de res.end().

Finalmente, hacemos que el servidor escuche en un puerto específico:

const puerto = 3000;
servidor.listen(puerto, () => {
  console.log(`Servidor ejecutándose en el puerto ${puerto}`);
});

El método servidor.listen() inicia el servidor y lo configura para que escuche en el puerto indicado. El callback opcional se ejecuta cuando el servidor empieza a escuchar.

El código completo quedaría así:

import { createServer } from "http";

const servidor = createServer((req, res) => {
    res.statusCode = 200;
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("¡Hola, mundo!");
});

const puerto = 3000;
servidor.listen(puerto, () => {
    console.log(`Servidor ejecutándose en el puerto ${puerto}`);
});

Para ejecutar el servidor, guardamos este código en un archivo, por ejemplo servidor.mjs, y lo ejecutamos con Node.js:

A partir de esta lección, utilizaremos la extensión .mjs para nuestros archivos de JavaScript. Esto se debe a que .mjs indica que estamos usando módulos ES6, lo cual nos permite aprovechar las características modernas de JavaScript, como import y export.

node servidor.mjs

Ahora, si visitamos http://localhost:3000 en un navegador, veremos el mensaje "¡Hola, mundo!".

Este sencillo servidor es el fundamento para aplicaciones más complejas. Al comprender cómo funciona http.createServer(), podemos expandir nuestra aplicación para manejar rutas, métodos HTTP y servir contenido dinámico. Además, podemos aprovechar características como la asincronía y los streams para crear aplicaciones eficientes y escalables.

Entendiendo req (request) y res (response)

Al crear un servidor HTTP en Node.js, los objetos req y res son esenciales para manejar las solicitudes y respuestas entre el cliente y el servidor. El objeto req representa la solicitud entrante, mientras que res representa la respuesta que el servidor enviará al cliente.

El objeto req contiene información vital sobre la solicitud del cliente:

  • req.url: la ruta solicitada, incluyendo la ruta de acceso y la cadena de consulta.
  • req.method: el método HTTP utilizado, como GET, POST, PUT, DELETE, etc.
  • req.headers: un objeto que contiene los encabezados de la solicitud, proporcionando detalles como el agente de usuario, contenido aceptado y más.

Por ejemplo, para registrar la ruta y el método de cada solicitud:

console.log(`Solicitud recibida: Método=${req.method}, Ruta=${req.url}`);

El objeto res permite configurar y enviar la respuesta al cliente. Algunas propiedades y métodos clave de res son:

  • res.statusCode: establece el código de estado HTTP de la respuesta (por defecto es 200).
  • res.setHeader(nombre, valor): añade o modifica un encabezado en la respuesta.
  • res.write(datos): escribe datos en el cuerpo de la respuesta sin finalizarla.
  • res.end([datos]): finaliza la respuesta, enviando opcionalmente datos adicionales.

Por ejemplo, para responder con un mensaje de texto plano:

res.statusCode = 200;
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
res.end('Respuesta exitosa');

Si se necesita enviar datos en múltiples partes, se puede utilizar res.write() varias veces antes de llamar a res.end():

res.write('Parte 1 de la respuesta\n');
res.write('Parte 2 de la respuesta\n');
res.end('Fin de la respuesta');

En cuanto al manejo del cuerpo de la solicitud, en métodos como POST o PUT, los datos se reciben como un flujo (stream). Se pueden escuchar los eventos ‘data’ y ‘end’ para recopilar los datos:

let cuerpo = '';
req.on('data', (chunk) => {
  cuerpo += chunk;
});
req.on('end', () => {
  // Procesar el cuerpo recibido
  console.log('Datos recibidos:', cuerpo);
});

Es importante manejar adecuadamente el flujo de datos para evitar problemas con solicitudes grandes o lentas. Además, se debe considerar el tipo de contenido para procesar correctamente los datos recibidos, verificando el encabezado req.headers['content-type'].

Para acceder a parámetros en la URL o en la cadena de consulta, se puede utilizar el módulo nativo url:

import { parse } from "url";

const urlParseada = url.parse(req.url, true);
const ruta = urlParseada.pathname;
const parametros = urlParseada.query;

Esto permite extraer la ruta y los parámetros de forma sencilla, facilitando el manejo de diferentes rutas y acciones basadas en los parámetros.

Al trabajar con encabezados, tanto en la solicitud como en la respuesta, es crucial respetar el protocolo HTTP. Por ejemplo, para configurar el encabezado Access-Control-Allow-Origin y permitir CORS:

res.setHeader('Access-Control-Allow-Origin', '*');

El manejo correcto de los objetos req y res es esencial para la creación de servidores HTTP robustos. Permite controlar cada aspecto de la comunicación, desde la interpretación de las solicitudes hasta la configuración detallada de las respuestas, asegurando que el servidor responda adecuadamente.

Rutas simples y manejo de métodos HTTP (GET, POST, PUT, DELETE)

En Node.js, podemos crear rutas simples y manejar diferentes métodos HTTP utilizando el módulo http. Al interpretar la propiedad req.url y req.method, dirigimos las solicitudes a distintos manejadores según la ruta y el método especificados por el cliente.

Para comenzar, ampliamos el servidor básico previamente creado:

import { createServer } from 'http';

const servidor = createServer((req, res) => {
  // Lógica para manejar rutas y métodos
});

const puerto = 3000;
servidor.listen(puerto, () => {
  console.log(`Servidor ejecutándose en el puerto ${puerto}`);
});

Analizamos la URL solicitada y el método HTTP utilizando la clase URL nativa de Node.js:

const servidor = createServer((req, res) => {
  const miURL = new URL(req.url, `http://${req.headers.host}`);
  const ruta = miURL.pathname;
  const metodo = req.method;

  // Lógica para manejar rutas y métodos
});

Creamos una lógica de enrutamiento basada en la ruta y el método. Por ejemplo, manejamos las siguientes rutas:

  • GET /: muestra un mensaje de bienvenida.
  • GET /usuarios: devuelve una lista de usuarios.
  • POST /usuarios: crea un nuevo usuario.
  • PUT /usuarios: actualiza un usuario existente.
  • DELETE /usuarios: elimina un usuario.

Implementamos esta lógica en el servidor:

const servidor = createServer(async (req, res) => {
  const miURL = new URL(req.url, `http://${req.headers.host}`);
  const ruta = miURL.pathname;
  const metodo = req.method;

  if (ruta === '/' && metodo === 'GET') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end('Bienvenido a nuestro servidor Node.js');
  } else if (ruta === '/usuarios' && metodo === 'GET') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    const usuarios = [{ id: 1, nombre: 'Juan' }, { id: 2, nombre: 'María' }];
    res.end(JSON.stringify(usuarios));
  } else if (ruta === '/usuarios' && metodo === 'POST') {
    const datos = await obtenerCuerpo(req);
    const nuevoUsuario = JSON.parse(datos);
    res.statusCode = 201;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify({ mensaje: 'Usuario creado', usuario: nuevoUsuario }));
  } else if (ruta === '/usuarios' && metodo === 'PUT') {
    const datos = await obtenerCuerpo(req);
    const usuarioActualizado = JSON.parse(datos);
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify({ mensaje: 'Usuario actualizado', usuario: usuarioActualizado }));
  } else if (ruta === '/usuarios' && metodo === 'DELETE') {
    res.statusCode = 200;
    res.setHeader('Content-Type', 'application/json; charset=utf-8');
    res.end(JSON.stringify({ mensaje: 'Usuario eliminado' }));
  } else {
    res.statusCode = 404;
    res.setHeader('Content-Type', 'text/plain; charset=utf-8');
    res.end('Ruta no encontrada');
  }
});

Definimos la función obtenerCuerpo para recopilar el cuerpo de la solicitud de manera asíncrona:

function obtenerCuerpo(req) {
  return new Promise((resolver, rechazar) => {
    let datos = '';
    req.on('data', (chunk) => {
      datos += chunk;
    });
    req.on('end', () => {
      resolver(datos);
    });
    req.on('error', (err) => {
      rechazar(err);
    });
  });
}

Al utilizar async/await, manejamos las operaciones asincrónicas de forma más limpia y clara. La función obtenerCuerpo nos permite leer el cuerpo de las solicitudes POST y PUT, que generalmente contienen datos en formato JSON.

Para manejar rutas más complejas, como recursos con identificadores, podemos utilizar expresiones regulares o analizar partes de la ruta. Por ejemplo, para manejar /usuarios/123:

const idUsuario = ruta.match(/^\/usuarios\/(\d+)$/);
if (idUsuario && metodo === 'GET') {
  const id = idUsuario[1];
  // Lógica para obtener el usuario con el id especificado
}

La expresión regular /^\/usuarios\/(\d+)$/ captura números en la ruta, permitiéndonos extraer el id del usuario. De esta manera nos ayuda a manejar rutas dinámicas y recursos específicos.

Finalmente, al desarrollar servidores HTTP en Node.js sin frameworks, adquirir una comprensión sólida del manejo de rutas y métodos nos permite crear APIs eficientes y adaptables. Aunque existen herramientas y frameworks que simplifican este proceso, entender los fundamentos nos proporciona mayor control y flexibilidad en nuestras aplicaciones.

Servir contenido estático/manual

Para mejorar la funcionalidad de nuestro servidor Node.js, es útil poder servir contenido estático como archivos HTML, CSS, JavaScript, imágenes y otros recursos. Esto permite que nuestros usuarios accedan a páginas web completas y recursos asociados desde nuestro servidor.

Para lograr esto manualmente, utilizaremos los módulos http y fs de Node.js. El módulo fs (sistema de archivos) nos permite leer archivos del disco y enviarlos como respuestas a las solicitudes de los clientes.

Primero, importamos los módulos necesarios:

import { readFile } from "fs";
import { createServer } from "http";
import { join, extname, normalize } from "path";
import { fileURLToPath } from "url";

Definimos un directorio base desde el cual serviremos nuestros archivos estáticos. Por ejemplo:

const __dirname = fileURLToPath(new URL('.', import.meta.url));
const directorioPublico = join(__dirname, "public");

Este directorio public contendrá todos los archivos que queremos servir.

A continuación, creamos el servidor HTTP:

const servidor = createServer((req, res) => {
    const rutaSolicitada = req.url;
    let rutaArchivo = join(directorioPublico, rutaSolicitada);

    // Manejo de rutas que terminan en '/'
    if (rutaSolicitada.endsWith("/")) {
        rutaArchivo = join(rutaArchivo, "index.html");
    }

    // Determinar la extensión del archivo
    const extension = extname(rutaArchivo).toLowerCase();

    // Mapear extensiones a tipos de contenido
    const tiposMime = {
        ".html": "text/html; charset=utf-8",
        ".css": "text/css; charset=utf-8",
        ".js": "application/javascript; charset=utf-8",
        ".json": "application/json; charset=utf-8",
        ".png": "image/png",
        ".jpg": "image/jpeg",
        ".gif": "image/gif",
        ".svg": "image/svg+xml",
        ".ico": "image/x-icon",
    };

    const tipoContenido = tiposMime[extension] || "application/octet-stream";

    // Leer y servir el archivo
    readFile(rutaArchivo, (error, contenido) => {
        if (error) {
            if (error.code === "ENOENT") {
                // Archivo no encontrado
                res.statusCode = 404;
                res.setHeader("Content-Type", "text/plain; charset=utf-8");
                res.end("404 - Archivo no encontrado");
            } else {
                // Otro error del servidor
                res.statusCode = 500;
                res.setHeader("Content-Type", "text/plain; charset=utf-8");
                res.end("500 - Error interno del servidor");
            }
        } else {
            // Éxito: Servir el archivo
            res.statusCode = 200;
            res.setHeader("Content-Type", tipoContenido);
            res.end(contenido);
        }
    });
});

Con este código, el servidor intenta encontrar y leer el archivo solicitado en el directorio public, y lo envía al cliente con el tipo MIME apropiado.

Es importante manejar diferentes extensiones y asignar el tipo de contenido correcto en el encabezado Content-Type. Esto asegura que el navegador interprete correctamente los archivos recibidos.

Para mejorar la seguridad, evitamos permitir que los usuarios accedan a archivos fuera del directorio público. Esto se logra al usar path.join y controlar adecuadamente las rutas.

Por ejemplo, para evitar ataques como Path Traversal, podemos normalizar la ruta y verificar que esté dentro del directorio público:

let rutaSegura = normalize(rutaArchivo);
if (!rutaSegura.startsWith(directorioPublico)) {
    res.statusCode = 403;
    res.setHeader("Content-Type", "text/plain; charset=utf-8");
    res.end("403 - Acceso prohibido");
    return;
}

Además, podemos implementar una página de error personalizada cuando un archivo no se encuentra:

if (error.code === "ENOENT") {
    readFile(join(directorioPublico, "404.html"), (error404, contenido404) => {
        res.statusCode = 404;
        res.setHeader("Content-Type", "text/html; charset=utf-8");
        if (error404) {
            res.end("404 - Archivo no encontrado");
        } else {
            res.end(contenido404);
        }
    });
}

De esta manera, servimos un archivo 404.html personalizado ubicado en el directorio público.

Para iniciar el servidor, especificamos el puerto y comenzamos a escuchar:

const puerto = 3000;
servidor.listen(puerto, () => {
    console.log(`Servidor ejecutándose en el puerto ${puerto}`);
});

Ahora, al colocar archivos en el directorio public, el servidor los servirá según las solicitudes de los clientes.

Por ejemplo:

  • http://localhost:3000/index.html servirá el archivo public/index.html
  • http://localhost:3000/css/estilos.css servirá public/css/estilos.css.
  • Si se accede a http://localhost:3000/, y la ruta termina en /, servirá public/index.html por defecto.

Para manejar rutas más complejas y mejorar la eficiencia, podemos utilizar el módulo fs.createReadStream para transmitir archivos grandes sin cargarlos completamente en memoria:

const streamLectura = createReadStream(rutaArchivo);
streamLectura.on('open', () => {
  res.statusCode = 200;
  res.setHeader('Content-Type', tipoContenido);
  streamLectura.pipe(res);
});
streamLectura.on('error', (error) => {
  res.statusCode = 500;
  res.setHeader('Content-Type', 'text/plain; charset=utf-8');
  res.end('500 - Error interno del servidor');
});

De esta forma, servimos archivos grandes de manera más eficiente, utilizando los streams de Node.js.

Es fundamental manejar los posibles errores y eventos en los streams para garantizar la estabilidad del servidor.

También podemos optimizar la configuración de tipos MIME utilizando un paquete externo como mime-types, pero dado que estamos trabajando sin frameworks ni paquetes adicionales, mantenemos el mapa de tipos MIME manualmente.

Para servir contenido estático de manera manual en Node.js, comprender el manejo de rutas, sistema de archivos y encabezados HTTP es importante. Esto nos permite construir servidores flexibles y adaptados a nuestras necesidades sin depender de frameworks externos.

Para seguir leyendo hazte Plus

¿Ya eres Plus? Accede a la app

Plan mensual

19.00 € /mes

Precio normal mensual: 19 €
47 % DE DESCUENTO

Plan anual

10.00 € /mes

Ahorras 108 € al año
Precio normal anual: 120 €
Aprende Node GRATIS online

Todas las lecciones de Node

Accede a todas las lecciones de Node y aprende con ejemplos prácticos de código y ejercicios de programación con IDE web sin instalar nada.

Accede GRATIS a Node y certifícate

En esta lección

Objetivos de aprendizaje de esta lección

  • Comprender el uso del módulo http en Node.js.
  • Crear un servidor básico con http.createServer().
  • Manejar solicitudes y respuestas HTTP.
  • Interpretar y gestionar los objetos req y res.
  • Configurar código de estado y encabezados HTTP.
  • Enviar respuestas al cliente correctamente.
  • Hacer que el servidor escuche en un puerto específico.