10 - Recuperar datos de un formulario HTML mediante Node.js (POST)


Hasta este momento hemos visto como podemos implementar un servidor de un sitio web que responde a peticiones de páginas y recursos estáticos que se encuentran en dicho servidor.

Ahora veremos como podemos hacer un programa en Node.js que pueda procesar los datos de un formulario HTML.

Problema

Implementar un formulario HTML que solicite ingresar el nombre de usuario y su clave. Cuando se presione el botón submit proceder a recuperar los datos del formulario y generar una página HTML dinámica que muestre los dos valores ingresados por el usuario.

Crearemos un directorio ejercicio12 y en el mismo crearemos un archivo llamado ejercicio12.js y un subdirectorio llamado 'public'.

El contenido del archivo HTML que muestra el formulario de login es (almacenar este archivo en la subcarpeta 'public'):

index.html
<!DOCTYPE html>
<html>
<head>
  <title>Prueba de Node.js</title>
  <meta charset="UTF-8">
</head>
<body>
  <form action="recuperardatos" method="post">
  Ingrese su nombre de usuario:
  <input type="text" name="nombre" size="30"><br>
  Ingrese clave:
  <input type="password" name="clave" size="30"><br>
  <input type="submit" value="Enviar">
</form>
</body>
</html>

Podemos hacer notar que la propiedad action tiene el valor:'recuperardatos'

  <form action="recuperardatos" method="post">

Este nombre no hace referencia a un archivo de nuestro servidor, sino veremos que cuando capturemos dicha 'url' generaremos el archivo HTML en forma dinámica.

Nuestro programa en Node.js que se encargará de servir las páginas y recuperar los datos del formulario es:

ejercicio12.js
const http = require('node:http')
const fs = require('node:fs')

const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

const servidor = http.createServer((pedido, respuesta) => {
  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)
})

servidor.listen(8888)


function encaminar(pedido, respuesta, camino) {
  console.log(camino)
  switch (camino) {
    case 'public/recuperardatos': {
      recuperar(pedido, respuesta)
      break
    }
    default: {
      fs.stat(camino, error => {
        if (!error) {
          fs.readFile(camino, (error, contenido) => {
            if (error) {
              respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
              respuesta.write('Error interno')
              respuesta.end()
            } else {
              const vec = camino.split('.')
              const extension = vec[vec.length - 1]
              const mimearchivo = mime[extension]
              respuesta.writeHead(200, { 'Content-Type': mimearchivo })
              respuesta.write(contenido)
              respuesta.end()
            }
          })
        } else {
          respuesta.writeHead(404, { 'Content-Type': 'text/html' })
          respuesta.write('<!doctype html><html><head></head><body>Recurso inexistente</body></html>')
          respuesta.end()
        }
      })
    }
  }
}


function recuperar(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    console.log(formulario)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina =
      `<!doctype html><html><head></head><body>
     Nombre de usuario:${formulario.get('nombre')}<br>
     Clave:${formulario.get('clave')}<br>
     <a href="index.html">Retornar</a>
     </body></html>`
    respuesta.end(pagina)
  })
}

console.log('Servidor web iniciado')

Lo primero que podemos ver que hemos subdividido las actividades en distintas funciones para hacer más legible nuestro programa (no lo implementamos con cache para dejar más limpio y entendible el programa)

Requerimos dos módulos:

const http = require('node:http')
const fs = require('node:fs')

const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

En la función anónima que le pasamos a createServer obtenemos el path del recurso solicitado por el navegador y le concatenamos el string 'public' que corresponde a la carpeta donde almacenamos nuestras páginas (en nuestro ejemplo hay una sola llamada index.html)

Llamamos a la función encaminar pasando los dos objetos 'pedido' y 'respuesta', también pasamos la variable camino que tiene el path del recurso solicitado:

  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)

Arrancamos el servidor y lo ponemos a escuchar en el puerto 8888:

servidor.listen(8888)

La función encaminar analiza mediante un switch (podíamos utilizar un if) el contenido del parámetro 'camino'.

function encaminar(pedido, respuesta, camino) {
  console.log(camino)
  switch (camino) {
    case 'public/recuperardatos': {
      recuperar(pedido, respuesta)
      break
    }
    default: {
      fs.stat(camino, error => {
        if (!error) {
          fs.readFile(camino, (error, contenido) => {
            if (error) {
              respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
              respuesta.write('Error interno')
              respuesta.end()
            } else {
              const vec = camino.split('.')
              const extension = vec[vec.length - 1]
              const mimearchivo = mime[extension]
              respuesta.writeHead(200, { 'Content-Type': mimearchivo })
              respuesta.write(contenido)
              respuesta.end()
            }
          })
        } else {
          respuesta.writeHead(404, { 'Content-Type': 'text/html' })
          respuesta.write('<!doctype html><html><head></head><body>Recurso inexistente</body></html>')
          respuesta.end()
        }
      })
    }
  }
}

El parámetro camino puede tener alguno de estos dos valores:

public/index.html
public/recuperardatos

Si el parámetro tiene el primer valor: 'public/index.html' luego se ejecuta el default del switch. Es decir retorna la página estática index.html como hemos visto en conceptos anteriores.

Si el parámetro 'camino' tiene el valor: 'public/recuperardatos' procede a llamar a la función recuperar y le pasa los dos objetos 'pedido' y 'respuesta'.

Finalmente la función recuperar (que se encarga de recuperar los dos datos del formulario y generar un archivo HTML para retornarlo al navegador):

function recuperar(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    console.log(formulario)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina =
      `<!doctype html><html><head></head><body>
     Nombre de usuario:${formulario.get('nombre')}<br>
     Clave:${formulario.get('clave')}<br>
     <a href="index.html">Retornar</a>
     </body></html>`
    respuesta.end(pagina)
  })
}

El objeto 'pedido' tiene un método llamado on. Debemos llamar este método dos veces, la primera pasando un string con el valor 'data' y una función anónima que se irá llamando a medida que lleguen los datos al servidor desde el navegador:

  pedido.on('data', datosparciales => {
    info += datosparciales
  })

Como vemos a medida que van llegando los datos los vamos concatenando en una variable llamada 'info'.

Cuando terminan de llegar todos los datos se ejecuta la función anónima que le pasamos al método on en la llamada con el string 'end':

  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    console.log(formulario)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina =
      `<!doctype html><html><head></head><body>
     Nombre de usuario:${formulario.get('nombre')}<br>
     Clave:${formulario.get('clave')}<br>
     <a href="index.html">Retornar</a>
     </body></html>`
    respuesta.end(pagina)
  })

En esta función anónima la variable info contiene todos los datos del formulario con una estructura similar a:

nombre=juan&clave=123456

Lo que nos queda ahora es crear un objeto de la clase URLSearchParams:

    const formulario = new URLSearchParams(info)

Luego de esto ya podemos acceder a cada elemento del formulario mediante el objeto 'formulario' llamando al método get e indicando el name de cada control del formulario:

formulario['nombre']     //Contiene el valor que cargó el usuario en el formulario
formulario['clave']      //Contiene la clave

Finalmente vemos que en una variable llamada 'pagina' almacenamos un código HTML válido que procedemos a enviarla al navegador que hizo la petición:

  respuesta.end(pagina)

Primero arrancamos el servidor:

arrancar servidor Node.js

Pedimos desde el navegador la página index.html (si no la indicamos ya codificamos que devuelva por defecto dicha página):

pagina estatica con Node.js

Finalmente cuando presionamos el botón 'Enviar' el navegador recibe la página generada en forma dinámica en el servidor:

pagina dinamica con Node.js

Este proyecto lo puede descargar en un zip con todos los archivos desde este enlace : ejercicio12

Implementación alternativa empleando el módulo 'fs/promises'

ejercicio12b.js
const http = require('node:http')
const fs = require('node:fs/promises')

const mime = {
  'html': 'text/html',
  'css': 'text/css',
  'jpg': 'image/jpg',
  'ico': 'image/x-icon',
  'mp3': 'audio/mpeg3',
  'mp4': 'video/mp4'
}

const servidor = http.createServer((pedido, respuesta) => {
  const url = new URL('http://localhost:8888' + pedido.url)
  let camino = 'public' + url.pathname
  if (camino == 'public/')
    camino = 'public/index.html'
  encaminar(pedido, respuesta, camino)
})

servidor.listen(8888)


function encaminar(pedido, respuesta, camino) {
  console.log(camino)
  switch (camino) {
    case 'public/recuperardatos': {
      recuperar(pedido, respuesta)
      break
    }
    default: {
      fs.stat(camino)
        .then(() => {
          fs.readFile(camino)
            .then(contenido => {
              const vec = camino.split('.')
              const extension = vec[vec.length - 1]
              const mimearchivo = mime[extension]
              respuesta.writeHead(200, { 'Content-Type': mimearchivo })
              respuesta.write(contenido)
              respuesta.end()
            })
            .catch(error => {
              respuesta.writeHead(500, { 'Content-Type': 'text/plain' })
              respuesta.write('Error interno')
              respuesta.end()
            })
        })
        .catch(error => {
          respuesta.writeHead(404, { 'Content-Type': 'text/html' })
          respuesta.end('<h1>Error 404: No existe el recurso solicitado</h1>')
        })
    }
  }
}


function recuperar(pedido, respuesta) {
  let info = ''
  pedido.on('data', datosparciales => {
    info += datosparciales
  })
  pedido.on('end', () => {
    const formulario = new URLSearchParams(info)
    console.log(formulario)
    respuesta.writeHead(200, { 'Content-Type': 'text/html' })
    const pagina =
      `<!doctype html><html><head></head><body>
     Nombre de usuario:${formulario.get('nombre')}<br>
     Clave:${formulario.get('clave')}<br>
     <a href="index.html">Retornar</a>
     </body></html>`
    respuesta.end(pagina)
  })
}

console.log('Servidor web iniciado')

Retornar