Skip to content

Latest commit

 

History

History
708 lines (479 loc) · 27.3 KB

README.md

File metadata and controls

708 lines (479 loc) · 27.3 KB

[WIP] Notas sobre ExpressjsJS

Contenido


Documentación

↑ Ir al inicio

Instalación

Express es paquete de Node, por lo que se instala con npm como cualquier otra dependencia de nuestro proyecto

npm install express

↑ Ir al inicio

¿Qué es ExpressJS?

Es un framework para desarrollar aplicaciones web (server-side) con Node, minimalista y no opinionado. Esto último significa que no asume que vayamos a construir nuestras aplicaciones de alguna forma en particular, cómo manejar los requests o generar los responses, etc, en ese sentido, mantenemos el control.

↑ Ir al inicio

Ventajas sobre usar utilizar Node (http)

  • Express está construido sobre el módulo http de Node, por lo que básicamente funciona como una API que simplifica y aumenta la funcionalidad que este módulo nos provee.
  • Nos permite construir aplicaciones web con Node de una forma mucho más simple
  • Es muy liviano y rápido
  • Minimalista: hace muy pocas cosas, se enfoca más bien en reducir el boilerplate y las partes más repetitivas y tediosas del código en Node (setear headers, MIME types, cerrar conexiones, servir archivos estáticos, simplifica mucho el routing, etc)
  • No opinionado: tenemos control total sobre cómo diseñar la aplicación, qué librerías y middlewares utilizar, cómo manejar los requests, responses, etc
  • Reduce la cantidad de código necesaria y nos abstrae de mucha de la complejidad que tenemos al utilizar el módulo HTTP
  • Tiene una API muy simple y expresiva
  • Middleware: funciones que nos van a permitir interceptar y manipular requests antes de hacer cualquier otra cosa
  • Express es el framework más popular para desarrollar aplicaciones web con Node

↑ Ir al inicio

Usos

  • servers
  • APIs
  • Microservicios

Generalmente, vamos a recibir requests con nuestra aplicación Express (back-end), generar la respuesta en formato JSON y enviársela al cliente (front-end) para que la consuma y haga con esta lo que quiera, genere las vistas correspondientes, etc. También podemos generar vistas desde el servidor, utilizando HTML plano como hicimos anteriormente en Node o algún template engine como Handlebars o Pug.

↑ Ir al inicio

Hola Mundo

Escribir el siguiente código en un archivo index.js

const express = require('express');
const app = express();
const HOSTNAME = '127.0.0.1';
const PORT = 8080;

app.get('/', (req, res) => res.send('Hello World!'));
app.listen(PORT, () => console.log('Server listening on http://${HOSTNAME}:${PORT}...'));

Al igual que en Node, utilizamos el método listen para dejar el servidor levantado y escuchando en el puerto que le indiquemos.

Luego, ejecutarlo con node index.js. ¡Ahora podemos entrar a localhost:8080 en nuestro navegador y ver la respuesta del servidor! 🚀

↑ Ir al inicio

Inicialización: ¿qué es app?

En cualquier tutorial, guía o documentación que busquemos sobre Express, nos vamos a encontrar con la variable app al inicio del código del programa. Si analizamos el código fuente del framework, veremos que lo que Express exporta (y estamos importando al hacer el require) es una función, createApplication(). Esta función retorna una instancia de una aplicación Express, que estamos almacenando en app.

Por lo tanto, app es un objeto, el cual nos permite acceder mediante diferentes propiedades y métodos a toda la funcionalidad que nos provee el framework.

const express = require('express');
const app = express();

Luego, para iniciar el servidor, debemos simplemente definir una ruta, su correspondiente callback y dejarlo escuchando en algún puerto. Al igual que en Node, el callback va a ejecutarse en cada request recibido en la ruta definida, creando una instancia de los objeto Request y Response, que se corresponden con los parámetros req y res, respectivamente.

const PORT = 8080;

app.get('/', (req, res) => res.send('Hello World!'));

app.listen(PORT);

↑ Ir al inicio

Request/Response

Request

Para cada request tenemos que definir:

  1. Verbo HTTP
  2. Ruta
  3. Callback

↑ Ir al inicio

Verbos HTTP

Podemos recibir diferentes tipos de requests, según el tipo de acción que se quiera realizar con un determinado recurso. Express nos proporciona diferentes métodos para cada uno de estos. Entre los más usuales, tenemos

  • GET: pedir un recurso
  • POST: enviar info
  • PUT: actualizar info
  • DELETE: eliminar info
  • etc
app.get('/', (req, res) => console.log('Give me more chocolate!'));
app.post('/', (req, res) => console.log('I'm posting something! :)'));
app.put('/', (req, res) => console.log('Update that!'));
app.delete('/', (req, res) => console.log('DELETE THIS #RIP'));

Cada uno de estos métodos recibe como parámetros

  1. una ruta específica (en el ejemplo '/')
  2. un callback, que será ejecutado cuando recibamos un request del tipo definido por el verbo HTTP (get, post, put, delete, etc), en esa ruta.

Express también posee un método app.all(), que matchea con cualquier tipode verbo HTTP.

↑ Ir al inicio

CRUD

Estos métodos se corresponden con los necesarios en aplicaciones CRUD.

CRUD Verbo HTTP
Create post
Read get
Update put
Delete delete

↑ Ir al inicio

Response

Utilizamos el método Response.send() para enviar una respuesta. Este método se encarga además de cerrar la conexión automáticamente.

app.get('/', (req, res) => res.send('Hello World!'));

El método setea el Content-Type según el tipo de parámetro que reciba:

  • si le pasamos un string, text/html
  • si recibe un objeto/array, application/json (y parsea el parámetro como JSON)

↑ Ir al inicio

Redirect

Podemos redireccionar utilizando el método Response.redirect() (con status 302 por default):

res.redirect('/another-url');

Un redirect 301 sería de la forma

res.redirect(301, '/go-there');

El path al que redireccionamos puede ser absoluto, relativo ó una URL.

↑ Ir al inicio

404

En el caso de recibir un request a una ruta inexistente, Express va a generar una respuesta automática de 404, del estilo

Cannot GET /undefined/route/

↑ Ir al inicio

Servir estáticos con express.static()

Para servir archivos estáticos, debemos guardarlos en algún directorio (en la raíz del proyecto) y utilizar el middleware express.static() (ya incluído en Express), referenciando este directorio

Por ejemplo, si guardamos los assets en /public

app.use(express.static('public'));

De esta forma, si tenemos por ejemplo un archivo index.html en /public, va a ser servido automáticamente si accedemos a la homepage http://localhost:PORT. No hay que incluir /public (ni ningún otro directorio que definamos como estático) en la URL, está implícito en esta.

También podemos setear múltiples directorios, tantos como necesitemos. Express va a buscar los archivos en el orden en el cual seteamos los directorios con express.static().

app.use(express.static('public'));
app.use(express.static('images'));

⚠️ Recordar que todo el contenido de nuestros directorios estáticos será público y accesible por cualquiera, mediante la URL correspondiente. Por lo tanto, no debemos guardar allí aquellos archivos e información que consideremos sensibles. Como convención y tip para tener siempre esto en cuenta, se suele llamar /public al directorio desde el cual servimos los estáticos.

↑ Ir al inicio

JSON

Podemos responder con un JSON utilizando el método Response.json(). Este método acepta un objeto/array y se encarga de convertirlo a JSON antes de enviarlo

app.get('/', (req, res) => res.json({salute: 'Hello World!'}));

Nota: para parsear un request que envía JSON, tenemos que utilizar el middleware body-parser, que nos permitirá acceder a su contenido a través de req.body.

Más info en express.json()`

↑ Ir al inicio

.send() vs .json()

Estos métodos son muy similares: Response.json() invoca a Response.send() al final.

La principal diferencia aparece cuando pasamos valores que no son objetos (tipos primitivos como null, undefined, etc), .json() va a convertirlos a formato JSON, mientras que .send() no, por lo tanto, es preferible utilizar .json() cuando querramos responder con este formato.

↑ Ir al inicio

HTTP Status

Cuando enviamos una respuesta, Express siempre setea automáticamente en los headers un status code por defecto (generalmente, 200). En el caso de que querramos utilizar uno en particular, podemos utilizar el el método Response.status() para setear el status code correspondiente

res.status(404).end();

Nota: el método Response.end() envía una respuesta vacía, sin contenido en el body.

res.status(404).send('File not found');

↑ Ir al inicio

Routing

Dependiendo de en qué URL se realizar el request, debemos responder de una forma distinta, es decir, los endpoints de nuestra aplicación deben reaccionar de diferentes maneras a los requests del cliente.

👉 El proceso de determinar qué debe suceder cuando un endpoint, con determinado verbo HTTP es llamado por el cliente, ó qué parte del código de nuestra aplicación debe manejar un request específico, es lo que se conoce como routing.

↑ Ir al inicio

Router

Para una aplicación simple, alcanza con utilizar la funcionalidad que nos provee Express a través de app para el manejo del routing, pero a medida que la complejidad de la aplicación crece, se vuelve más engorroso. Por esto y para modularizar más nuestra aplicación, hacerla más mantenible y separar responsabilidades, la lógica de routing suele implementarse por separado, en otro módulo.

A esta parte del código, encargada del routing (manejo de rutas y requests), se la conoce como router.

Express incluye como middleware al objeto router.

Nota: el uso del router que nos provee Express es opcional, podemos seguir manejando el routing con Node o usar otro router engine (buscar en NPM). El router de Express es bastante minimal y nos permite separar la configuración y la lógica de la aplicación del manejo del routing.

También podemos organizar las rutas de nuestra aplicación en diferentes módulos, usando el ´Router´ en cada uno de ellos y luego usándolos como middleware. Por ejemplo, si tuviésemos las rutas correspondientes a las diferentes secciones de un diario, organizadas en /sports, /arts, /books, etc, luego haríamos

const sportsRoutes = require('./sports');
const artsRoutes = require('./routes');
const booksRoutes = require('./routes');

app.use(sportsRoutes);
app.use(artsRoutes);
app.use(booksRoutes);

Nota: en todos los módulos donde estemos utilizando el ´Router´ tenemos que importar ´express´, ya que el ´Router´ no deja de ser un middleware de este. Por ejemplo

// routes/index.js
const express = require("express");

const router = express.Router();

const users = []; // this would ideally be a database, but we'll start with something simple
let id = 1; // this will help us identify unique users

// instead of `app.get`...
router.get("/users", (req, res) => {
  res.json(users);
});

router.get("/users/:id", (req, res) => {
  const user = users.find(val => val.id === Number(req.params.id));
  res.json(user);
});

// instead of `app.post`...
router.post("/users", (req, res) => {
  users.push({
    name: req.body.name,
    id: ++id
  });
  res.json({ message: "Created" });
});

// instead of `app.patch`...
router.patch("/users/:id", (req, res) => {
  const user = users.find(val => val.id === Number(req.params.id));
  user.name = req.body.name;
  res.json({ message: "Updated" });
});

// instead of `app.delete`...
router.delete("/users/:id", (req, res) => {
  const userIndex = users.findIndex(val => val.id === Number(req.params.id));
  users.splice(userIndex, 1);
  res.json({ message: "Deleted" });
});

// Now that we have built up all these routes - let's export this module for use in our app.js!
module.exports = router;

↑ Ir al inicio

Router: sintaxis más declarativa

El Router nos permite también utilizar una sintaxis más declarativa y legible a través del método route.

const express = require("express");
const router = express.Router();

const users = [];
let id = 1;

// declare the route first, then all the methods on it
router
  .route("/users")
  .get(() => {
    return res.json(users);
  })
  .post(() => {
    users.push({
      name: req.body.name,
      id: ++id
    });
    return res.json({ message: "Created" });
  });

router
  .route("/users/:id")
  .get((req, res) => {
    const user = users.find(val => val.id === Number(req.params.id));
    return res.json(user);
  })
  .patch((req, res) => {
    user.name = req.body.name;
    return res.json({ message: "Updated" });
  })
  .delete((req, res) => {
    users.splice(user.id, 1);
    return res.json({ message: "Deleted" });
  });

module.exports = router;

También podemos utilizar prefijos para las rutas que definamos

// prefix every single route in here with /users
app.use("/users", userRoutes);

...y luego definir las rutas de la forma

const express = require("express");
const router = express.Router();

const users = [];
let id = 1;

// declare all the methods on the /users route (prefix specified in app.js)
router
  .route("")
  .get((req, res) => {
    return res.json(users);
  })
  .post((req, res) => {
    users.push({
      name: req.body.name,
      id: ++id
    });
    return res.json({message: "Created"});
  });

router
  .route("/:id")
  .get((req, res) => {
    const user = users.find(val => val.id === Number(req.params.id));
    return res.json(user);
  })
  .patch((req, res) => {
    const user = users.find(val => val.id === Number(req.params.id));
    user.name = req.body.name;
    return res.json({ message: "Updated" });
  })
  .delete((req, res) => {
    const userIndex = users.findIndex(val => val.id === Number(req.params.id));
    users.splice(userIndex, 1);
    return res.json({ message: "Deleted" });
  });

});

module.exports = router;

↑ Ir al inicio

Rutas

Definimos rutas usando los métodos que nos provee el objeto que genera Express, que se corresponden con métodos HTTP: por ejemplo podemos usar app.get() para manejar requests de tipo GET y app.post() para requests de tipo POST.

Nota: los métodos de app pueden encadenarse

const express = require('express');
const app = express();
const PORT = 8080;

app
  .get('/', (req, res) => res.send('Hola Mundo!'))
  .listen(PORT, () => console.log(`Express app listening on port ${PORT}!`));

También existe el método .all, que va a aceptar cualquier tipo de request

// '*' es una ruta comodín, cualquier request a cualquier ruta que hagamos va a caer acá
app.all('*', (req, res) => res.send('Hello'));

Ver todos los métodos HTTP disponibles

↑ Ir al inicio

URL Parameters

Podemos proveer diferentes respuestas a través de una misma ruta si utilizamos parámetros. Vamos a obtener estos parámetros a través de la URL. Esto se conoce como dynamic routing (ruteo dinámico). Por el contrario, las rutas que no tienen parámetros son rutas estáticas.

req.params

Estos parámetros se almacenan en el objeto params, el cual existe dentro del objeto request. Por ejemplo, si queremos acceder al parámetro color y utilizarlo en la respuesta, podemos hacer lo siguiente:

app.get('/favorites/:color', (req, res) => {
  const { color } = req.params;
  res.send(`${color} es tu color favorito!`);
});

Para especificar qué parte de una URL será un parámetro, agregamos el caracter : y luego le damos un nombre. Este valor se va a mapear a una propiedad con ese nombre dentro del objeto params.

Podemos tener múltiples parámetros en una misma URL. Por ejemplo:

// Route path
/users/:userId/books/:bookId
// Request URL
http://localhost:3000/users/34/books/8989

req.params: { "userId": "34", "bookId": "8989" }

Para más info, ver Route parameters

↑ Ir al inicio

req.query

[EN DESARROLLO]

Es importante notar que los parámetros recibidos por URL son siempre strings.

⚠️ El orden en el que declaramos las rutas importa

[EN DESARROLLO]

Si tenemos, por ejemplo, definidas las siguientes rutas:

app.get('/', (req, res) => res.send('Hello World!'));
app.all('/', (req, res) => res.send('Bye World!'));

la segunda nunca será alcanzada.

👉 Express funciona de forma top-down, se ejecuta el callback correspondiente de la primer ruta que coincida y omite el resto, a menos que explícitamente definamos que debe continuar.

↑ Ir al inicio

Middleware

Una función middleware es cualquier tipo de función que intercepta el proceso de routing y tiene acceso al objeto request (req), al objeto response (res) y a la siguiente función middleware (next) en el ciclo request-response de nuestra aplicación. Estas funciones hacen de intermediarios en el ciclo request/response (de ahí el término middleware), con la finalidad de realizar algún tipo de operación en algún punto de esta cadena.

Los usos más comunes incluyen

  • acceder a cierta info que nos proveen (o editar) los objetos Request y Response
  • chequear si un usuario está logueado
  • almacenar info en la DB
  • parsear info proveniente del body del request
  • etc

👉Podemos decir que Express es un framework integrado esencialmente por 2 cosas: Router y conjunto de funciones middleware

👉 Utilizamos app.use(PATH) para indicar que vamos a utilizar un middleware determinado, de un PATH específico y agregarlo al stack de ejecución:

app.use((req, res, next) => { /* */ });

Por ejemplo, si queremos utilizar el middleware blackMagic a nivel aplicación, hacemos

app.use(blackMagic);

Mientras que si queremos utilizarlo en una ruta específica, por ejemplo /magic, hacemos

// this will run `validateUser` on /magic, ALL HTTP methods
app.use('/magic', blackMagic);
// this will run `validateUser` on /, GET method
app.get('/magic', blackMagic);

next y custom middleware

next es una referencia que utilizamos para pasar el control a la siguiente función middleware (siempre y cuando la ruta coincida). Al ser invocada, ejecuta el middleware que le sucede al actual. Vamos a llamar a next al final del middleware actual si queremos pasarle el control a la siguiente función middleware, sino, finaliza el ciclo y se envía la respuesta al cliente.

Express nos provee de algunos middlewares por default. También podemos encontrar otros como paquetes de NPM, o definir los nuestros propios (custom).

Ejemplo de middleware propio:

function validateUser(req, res, next) {
  // some middleware code (get info from req, do smth with db, etc...)
 res.locals.validatedUser = true;
 next();
}

// this will run `validateUser` on ALL paths, ALL HTTP methods
app.use(validateUser);

app.get('/', (req, res, next) => {
  const { validatedUser } = res.locals;
  console.log(`valid user: ${validatedUser}`);
  
  res.send('Hello, user!');
  // we don't call `next` here, so the cycle ends
})

Otro ejemplo:

function reqLogger(req, res, next) {
 console.log(req)
 next();
}

// Use `reqLogger` middleware
app.use(reqLogger);

app.get('/', (req, res, next) => res.send('Hello, world!'));

👉 Algo importante a tener en cuenta siempre es que el orden en el que definimos las rutas e invocamos el middleware es importante. Si ubicamos app.get por encima del app.use el middleware nunca se va a ejecutar, es por esto que casi siempre incluímos el middleware antes de la lógica de routing.

Usando middleware a través NPM

En el caso de utilizar un middleware externo (a través de NPM), debemos seguir los siguientes pasos

  1. Instalarlo (ej: npm i body-parser)
  2. Importarlo (ej: const bodyParser = require('body-parser'))
  3. Usarlo (ej: app.use(bodyParser.json()))

También podemos setear un middleware para que se ejecute sólo con algunas rutas específicas (no todas), si lo usamos como 2do. parámetro en la definición de la ruta:

app.get('/', middlewareFn, (req, res) => res.send('Hello World!'));

Para más info, ver Using middleware

↑ Ir al inicio

Middleware: beneficios

La principal ventaja de utilizar middleware en Express es la de modularizar nuestro código en pequeñas funciones, que podemos utilizar a nivel de la aplicación entera, de una forma mucho más limpia y mantenible, sin necesidad de definir esta funcionalidad en cada ruta.

Middleware: ejemplos

↑ Ir al inicio

Tipos de middleware

Una aplicación Express puede utiilizar los siguientes tipos de middleware:

  • Application-level middleware (también conocido como Root-level)
  • Router-level middleware
  • Error-handling middleware
  • Built-in middleware
  • Third-party middleware
  • Custom middleware

↑ Ir al inicio

Templating

[EN DESARROLLO]

Enviar HTML estático

En el caso de querer enviar un HTML estático como respuesta (sin utilizar ningún template), debemos utilizar el método [Response.sendFile()](http://expressjs.com/en/api.html#res.sendFile) e indicarle la ruta absoluta del archivo (ya que estamos leyendo un archivo del file system en lugar de generar uno nuevo a partir de un template). Para esto último, podemos utilizar el método de Node path.join() junto con __dirname.

Práctica

Setup

  1. Crear un archivo .env. En este archivo vamos a setear las variables de entorno de nuestro proyecto.
  2. Instalar y utilizar nodemon para correr nuestras aplicaciones, con el script dev del package.json
  3. Instalar y utilizar dotenv para leer variables de entorno de un archivo .env
const dotenv = require('dotenv');
dotenv.config();

Luego accederemos a las variables definidas en el archivo .env a través de process.env. Por ejemplo

// .env
PORT=8080
// server.js
const { PORT } = process.env;

⚠️ Nota: el archivo .env no debe comitearse, agregarlo al .gitignore

↑ Ir al inicio

Ejercicios

  1. Crear un servidor en Express, que escuche en el puerto 8080 (leerlo del archivo .env) y responda con un 'Hola Mundo!' cuando reciba un request a la ruta /. En el caso de que no haya un puerto seteado en .env, la aplicación debe escuchar en el puerto 8001. Cuando el servidor esté levantado y corriendo, la aplicación debe loguear por consola Express app listening on port ${PORT}!, donde PORT es el puerto seteado.

  2. Modificar el ejercicio anterior, para que al recibir un request GET a la ruta /salute/{name}, la aplicación responda con un 'Hola {name}!', donde name es un parámetro que recibe por URL.

  3. Rehacer el ejercicio 2 utilizando Express. Usar pug como template engine para retornar las diferentes vistas HTML de nuestra aplicación.

  4. Rehacer el ejercicio 6, sirviendo los archivos estáticos (assets) con Express desde la carpeta /public.

  5. Ver express-salad

↑ Ir al inicio