/notes-nodejs

Notas sobre NodeJS

MIT LicenseMIT

Notas sobre NodeJS (Server-Side JavaScript)

👉 Ver todas las notas

Contenido


Instalación

  • Windows: ir a la página oficial, descargar e instalar el binario de la versión LTS
  • Linux / OS X: descargar e instalar el script nvm y luego instalar la versión LTS con el comando nvm install --lts

Para chequear que se haya instalado correctamente, correr el comando node -v en la consola (debe retornar una versión >= 12)

¿Qué es LTS? Leer LTS vs Current version

↑ Ir al inicio

¿Qué es NodeJS?

NodeJS ó Node a secas, es principalmente un entorno de ejecución, es decir, nos brinda el contexto necesario para poder ejecutar código JavaScript por fuera de un browser.

Es Open-Source y tiene soporte multi-plataforma.

Recordemos que JavaScript sólo funciona en el browser de forma nativa y que se trata de un lenguaje de alto nivel. Esto significa que realizar tareas como networking, acceder a hardware de red (ej: acceder a la tarjeta de red) para poder escuchar requests y responder, acceder al file system del sistema operativo que estemos usando, para leer archivos (ej: un documento HTML) y enviarlos, poder levantar y correr un server, comunicarnos con una base de datos, desarrollar una API, etc, no son capacidades propias de un lenguaje como JavaScript.

Es por esto que necesitamos un intermediario, algo así como una API que nos permita, escribiendo el código JavaScript que ya conocemos, acceder a este tipo de funcionalidades, necesarias para el backend de nuestra aplicación.

Vamos a llamar entorno de ejecución a todo lo que necesitamos para poder ejecutar nuestro código e interactuar con estas funcionalidades. Node es quien nos va a dar este entorno y nos va a permitir ejecutar código JavaScript prácticamente en todos lados.

↑ Ir al inicio

V8

Uno de los componentes de Node es V8, un engine de JavaScript que se encarga de parsear, compilar, optimizar, interpretar y ejecutar nuestro código.

La computadora no entiende (y por lo tanto no puede ejecutar) JavaScript directamente. Un engine, como lo es V8, toma nuestro código JavaScript y lo convierte a algo que si entiende, lo que se conoce como código máquina o binario.

Aparte de V8, tenemos el lenguaje C++, el cual, a traves de ciertas librerias que vienen con Node (como libuv), nos permiten acceder a funcionalidad de más bajo nivel como las mencionadas antes e interactucar directamente con el sistema operativo. Para acceder a esa funcionalidad, vamos a utilizar la API que nos provee Node.

↑ Ir al inicio

Operaciones I/O

Las operaciones de I/O son aquellas que se producen cuando hay una comunicación entre una computadora y ciertos periféricos, como pueden ser una tarjeta de red ó un disco.

Estas operaciones pueden consistir en, por ejemplo, intercambiar información a través de una red (networking) realizando requests, escuchar requests en cierto puerto y generar una respuesta, acceder a una base de datos ó realizar diversas acciones con el filesystem, como crear, leer y escribir archivos, copiar y pegar archivos, creary eliminar directorios, etc.

↑ Ir al inicio

Single-thread

Las aplicaciones desarrolladas en Node corren en un único proceso (aka thread). Es decir, no crean un proceso nuevo por cada request. Node nos provee de muchos métodos de I/O asincrónicos a través de su API que previenen que el código se bloquee. La mayoría de las librerías, módulos y frameworks de Node estan escritas utilizando el paradigma asincrónico, por lo que encontrar código bloqueante o sincrónico es más bien la excepción y no la norma.

↑ Ir al inicio

Non-blocking I/O

Cuando Node realiza alguna operación de I/O, en lugar de bloquear el único thread del que disponemos y desperdiciar ciclos de CPU esperando, Node retomará las operaciones pendientes cuando la respuesta se encuentre disponible.

Esto le permite a Node por ejemplo,poder manejar tranquilamente miles de conexiones concurrentes con un único servidor levantado (recordemos que operamos con un único proceso), lo cual nos proporciona un gran ahorro de recursos, hardware, etc y evitamos tener que lidiar con el manejo de threads para la concurrencia, simplificando en gran medida y agilizando el desarrollo.

↑ Ir al inicio

Entorno + API

Por lo tanto, podríamos decir que Node termina siendo un entorno de ejecución para poder correr código JavaScript y una API (ó librería) que nos provee acceso a funcionalidades necesarias para el backend de nuestra aplicación.

Gracias a esta API, Node nos permite construir desde pequeñas aplicaciones de línea de comandos (CLI), hasta servidores HTTP para crear sitios dinámicos y aplicaciones web.

↑ Ir al inicio

REPL

REPL es una sigla que viene de Read, Eval, Print, Loop. Nos permite ejecutar Node en la terminal para probar cosas, como si se tratase de la consola del browser.

// 1
console.log("Hello World");
// 2
node helloworld.js

↑ Ir al inicio

Node nos permite escribir código asincrónico

// sync version
const result = database.query("SELECT * FROM veryHugeTable");
console.log("Hello World");
// async version
database.query("SELECT * FROM hugetable", function(rows) {
  const result = rows;
});
console.log("Hello World");

↑ Ir al inicio

Leyendo argumentos

Para leer argumentos a través de la terminal/CLI, podemos utilizar process.argv, que nos da acceso a un Array. Notar que este array también incluye como argumentos el comando que usamos para correr nuestro script y la ruta del archivo, por lo que los argumentos que nos interesan comienzan recién a partir del índice 2 del mismo, los cuales podemos obtener haciendo

const args = process.argv.slice(2);

Para más info, ver Node.js, accept arguments from the command line

↑ Ir al inicio

Iterar argumentos

process.argv.forEach((val, index) => {
  console.log(`${index}: ${val}`);
})

↑ Ir al inicio

File System

Podemos usar Node para leer y escribir archivos, a través del módulo fs

Ver the File System module

  • Crear un archivo readMe.txt (en el mismo directorio donde tengamos nuestro index.js) con el contenido de este txt

↑ Ir al inicio

Sync

const fs = require('fs');

const { readFileSync } = fs;
const txt = readFileSync('readMe.txt', 'utf-8');

console.log(txt);

↑ Ir al inicio

Escribir un archivo

const fs = require('fs');

const { readFileSync } = fs;
const readMe = readFileSync('readMe.txt', 'utf-8');
writeFileSync('writeMe.txt', readMe);

console.log(txt);

↑ Ir al inicio

Async

const fs = require('fs');

const { readFile } = fs;

readFile('readMe.txt', 'utf8', (err, data) => {
  if (err) {
    throw err
  };
  
  console.log(data)
});

// const readFile = util.promisify(fs.readFile)
// readFile("path/to/myfile").then(file => console.log(file))
const fs = require('fs');

const { readFile: read, writeFile: write } = fs;

read('readMe.txt', 'utf-8', (err, data) => {
  if (err) {
    console.error(err);
  }

  write('writeMe.txt', data, err => console.error(err));
})

console.log('HELLO!');

↑ Ir al inicio

Borrar archivos

⚠️ Sólo podemos eliminar archivos que existan, caso contrario unlink va a retornar un error

const fs = require('fs');

const { unlink: del } = fs;
const filePath = 'writeMe.txt';

del(filePath, err => {
  if (err) {
    throw err;
  }

  console.log(`${filePath} was deleted succesfully.`)
});

↑ Ir al inicio

Copiar archivos

Usar fs.copyFile para copiar 'readMe.txt' a 'readMeCopy.txt'

↑ Ir al inicio

Renombrar/mover archivos

Usar fs.rename para renombrar 'readMeCopy.txt' a 'readMe_copy.txt'

↑ Ir al inicio

Crear directorios

const fs = require('fs');

const { mkdir, rmdir, readFile: read, writeFile: write } = fs;

mkdir('node-fs', err => {
  if (err)
  read('readMe.txt', 'utf-8', (err, data) => {
    write('./node-fs/writeMe.txt', data)
  })
})

↑ Ir al inicio

Crear directorio y mover un archivo

const fs = require('fs');

const { mkdir, rename } = fs;

function move(src, dst) {
  rename(src, dst, err => {
    if (err) {
      throw err;
    }
  });
}

mkdir('node-fs', err => {
  if (err) {
    throw err;
  }

  move('readMe_copy.txt', 'node-fs/readMe_copy.txt');
});

↑ Ir al inicio

Borrar directorios

⚠️ Si intentamos borrar un directorio que no está vacío, se va a generar un error. Ver Remove a directory that is not empty in NodeJS

const fs = require('fs');

const { rmdir } = fs;

rmdir('node-fs');
const fs = require('fs');

const { rmdir, unlink: del } = fs;

del('./node-fs/writeMe.txt', err => {
  if (err) {
    throw err;
  }
 
  rmdir('node-fs');
})

↑ Ir al inicio

Ejercicios

  1. Usar prepend-file para agregar el texto
`Con 15 peso', con 15 peso' me hago 

`

al principio del texto del archivo y mostrar el resultado en la consola

  1. Escribir en el archivo ticket.txt el texto Gastaste ${importe} en ${producto}!, donde importe y producto son parámetros que se reciben por consola

  2. Escribir la función ls que tome como parámetro por consola un string que represente la ruta de un directorio local y loguee en consola los archivos del directorio. Investigar para esto el método fs.readdir

↑ Ir al inicio

Path

El módulo pathde Node es muy útil para trabajar con y manipular rutas (paths) de diferentes maneras.

const path = require("path");

// Normalizar una ruta
console.log(path.normalize("/test/test1//2slashes/1slash/tab/..")); // /test/test1/2slashes/1slash

// Unir múltiples paths
console.log(path.join("/first", "second", "something", "then", "..")); // /first/second/something

// Resolver un path (obtener la ruta absoluta de un archivo)
console.log(path.resolve("first.js"));

// Obtener la extensión de un archivo
console.log(path.extname("main.js")); // .js

↑ Ir al inicio

HTTP (Server y requests)

El módulo http nos provee de la funcionalidad necesaria para crear servidores HTTP y realizar requests.

// server v1
const http = require("http");

// `createServer()` crea un nuevo servidor HTTP y retorna un objeto, que tiene el método `listen`
const server = http.createServer();
server.listen(8888);
  • Abrir http://localhost:8888/ en el browser
node server.js
  • Cuando el servidor recibe un request, se dispara el evento request y se ejecuta el callback que recibe createServer. Este callback tiene 2 parámetros, los objetos request y response
// server v2
// en un archivo server.js
const http = require("http");

http.createServer((request, response) => {
  response.writeHead(200, {'Content-Type': 'text/plain'});
  response.write('Hello World');
  response.end();
}).listen(8888);
node server.js
// 7. server v3 (refactoring)
const http = require("http");
const HOSTNAME = '127.0.0.1'
const PORT = 8888;

function onRequest(request, response) {
  console.log("Request received.");
  response.writeHead(200, {"Content-Type": "text/plain"});
  response.write("Hello World");
  response.end();
}

http.createServer(onRequest).listen(PORT, error => {
  if (error) {
    console.error('THIS IS FINE. 🔥🔥🔥🚒');
  } else {
    console.log(`Server listening on http://${HOSTNAME}:${PORT}`);
  }
});
// server v4 (ejemplo de la documentación de Node)
const http = require('http');

const HOSTNAME = '127.0.0.1';
const PORT = process.env.PORT;

const server = http.createServer((request, response) => {
  response.statusCode = 200;
  response.setHeader('Content-Type', 'text/plain');
  response.end('Hello World!');
})

server.listen(port, hostname, () => {
  console.log(`Server running at http://${HOSTNAME}:${PORT}/`);
})
PORT=8888 node app.js

↑ Ir al inicio

Haciendo requests con Node

Ver Making HTTP requests with Node

↑ Ir al inicio

Haciendo requests con node-fetch

Ver node-fetch

↑ Ir al inicio

Ejercicios

Tip: Usar nodemon y crear el script dev: nodemon index.js en el package.json para correrlos

  1. Crear un servidor en Node, que escuche en el puerto 8001 (el puerto debe pasarse como parámetro a través de las variables de entorno) y responda con el siguiente HTML:
<h1>Hey!</h1>
<p>Soy un servidor <code>Node</code> y vos estás haciendo un <code>{{METHOD}}</code> a la <em>url</em> <code>{{URL}}</code> 🎉</p>

donde {{URL}} es la url a la cual el cliente hizo el request, ej: localhost:8001/node y {{METHOD}} es el verbo HTTP utilizado, ej: GET. Para visualizar correctamente el HTML, tendremos que agregar el charset al Content-Type en los headers:

'Content-Type': 'text/html; charset=utf-8'
  1. Modificar el código del ejercicio anterior, para que si se hace un request a /hello, el servidor responda con el siguiente HTML:
<h1>Hola! 😃</h1>
<p>Soy un servidor <code>Node</code> y vos estás haciendo un <code>{{METHOD}}</code> a la <em>url</em> <code>{{URL}}</code> 🎉</p>

y si el request se hace a /bye, la respuesta sea

<h1>Bueno. 😢</h1>
<p>Soy un servidor <code>Node</code> y vos estás haciendo un <code>{{METHOD}}</code> a la <em>url</em> <code>{{URL}}</code> 🎉</p>

Nota: en ambos casos, lo único que cambia en la respuesta es el contenido del h1 y si el request se hace a /, la respuesta debe ser la misma del primer ejercicio.

  1. Modificar el código del ejercicio anterior, para que, si se realiza un request a la url /christmas, el servidor devuelva, en formato JSON, la cantidad de minutos que faltan entre la fecha actual y el 25 de Diciembre, 0hs. Usar date-fns para realizar este cálculo. (Nota: para importar el módulo correspondiente, usar const differenceInMinutes = require('date-fns/differenceInMinutes'), en la documentación está mal)

  2. Modificar el código del ejercicio 1, para tener el servidor en un archivo server.js, que exporte la función up, la cual sirve para iniciar el servidor en el puerto 8888. Esta función debe loguear mensajes por consola indicando cuando el servidor está levantado y cuando recibe un nuevo request. El callback que recibe createServer debe modularizarse y moverse a la función onRequest. Por último, Crear el archivo index.js, en el cual vamos a importar el server y utilizar la función up para correrlo.

  3. Crear un archivo index.html con el siguiente contenido

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <meta http-equiv="X-UA-Compatible" content="ie=edge" />
    <title>Server</title>
    <link href="https://fonts.googleapis.com/css?family=Molle:400i&display=swap" rel="stylesheet" />
    <style>
      body {
        font-family: 'Molle', cursive;
        color: #026e00;
        padding: 24px;
      }
      h1 {
        font-size: 5em;
      }
      p {
        font-size: 2em;
      }
      h1,
      p {
        text-align: center;
      }
    </style>
  </head>
  <body>
    <h1>Hey!</h1>
    <p>Soy un servidor <code>Node</code> y estás viendo el <code>index.html</code> que leí y te estoy mandando 🎉</p>
  </body>
</html>

Luego, modificar el código del ítem anterior, para que como respuesta envíe el resultado de leer el contenido del archivo HTML creado.

  1. Modificar el html del ejercicio anterior para incluir esta imagen con el nombre node.png en el mismo. La imagen debe estar en el proyecto, dentro de una carpeta assets. En el caso de que el servidor reciba un request a una ruta no definida, debe responder con un status 404 y el html <h2>404 - Page not found :(</h2>

↑ Ir al inicio

Módulos

Los módulos forman parte de los bloques fundamentales que utilizamos en Node para construir aplicaciones. Como buena práctica, vamos a tratar siempre en lo posible de construir módulos pequeños, con un propósito único y claro.

Los módulos en Node se importan utilizando la función require(). Para ser cargado como módulo, un paquete debe contener un archivo index.js ó el campo main definido en el package.json, para indicar un entry point específico.

⚠️ La carga de los módulos se realiza de forma sincrónica usando require, por lo que siempre debemos cargarlos al inicio del archivo si no queremos bloquear la aplicación

↑ Ir al inicio

Módulos vs. paquetes

Un archivo js importado con require() es un módulo pero no un paquete, porque no tiene un archivo package.json.

Un paquete es una carpeta/directorio que contiene lo siguiente:

  • Un archivo package.json, que describe la aplicación y sus dependencias
  • Un entry point definido, que por default es el archivo index.js
  • Un subdirectorio node_modules, que es la ubicación default donde Node va a buscar las dependencias del proyecto
  • El resto de los archivos que forman parte del código fuente de la aplicación

↑ Ir al inicio

Importar y exportar

// In `greetings.js`, create three functions
const sayHello = name => `Hello, ${name}`;
const flatter = () => `Look how gorgeous you are today!`;
const sayGoodbye = name => `Goodbye, ${name}`;

// Export two of them
module.exports = {
  sayHello,
  flatter
};

// Load the module "greetings.js"
const greetings = require("./greetings.js");

// Use exported functions
console.log(greetings.sayHello("Baptiste")); // "Hello, Baptiste"
console.log(greetings.flatter()); // "Look how gorgeous you are today!"

↑ Ir al inicio

NPM

  • Crear package.json usando el comando npm init
  • ⚠️ Agregar node_modules al .gitignore
  • ⚠️ Los archivos package.json y package-lock.json deben comitearse SIEMPRE!

↑ Ir al inicio

NPM Cheatsheet

Instalar módulo como dependencia de nuestro proyecto

# son equivalentes
npm install <MODULE_NAME>
npm i <MODULE_NAME>

Instalar módulo como dependencia de desarrollo de nuestro proyecto

# son equivalentes
npm install --save-dev <MODULE_NAME>
npm i --save-dev <MODULE_NAME>

Instalar módulo de forma global

# son equivalentes
npm install --global <MODULE_NAME>
npm i -g <MODULE_NAME>

Actualizar módulo a la siguiente versión disponible, según cómo tengamos declarada la dependencia en el package.json (ver SEMVER)

npm update <MODULE_NAME>

Desinstalar una dependencia del proyecto

npm uninstall <MODULE_NAME>

Desinstalar un módulo global

npm uninstall -g <MODULE_NAME>

↑ Ir al inicio

NPX

NPX sirve para ejecutar ciertos comandos (fundamentalmente relacionados a CLIs) sin tener la necesidad de descargar e instalar un módulo en nuestro proyecto

↑ Ir al inicio