👉 Ver todas las notas
- Documentación
- Instalación
- ¿Qué es ExpressJS?
- Ventajas sobre usar utilizar Node (
http
) - Usos
- Routing
- Middleware
- Práctica
Express es paquete de Node, por lo que se instala con npm
como cualquier otra dependencia de nuestro proyecto
npm install express
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.
- 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
- 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.
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! 🚀
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);
Para cada request tenemos que definir:
- Verbo HTTP
- Ruta
- Callback
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 recursoPOST
: enviar infoPUT
: actualizar infoDELETE
: 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
- una ruta específica (en el ejemplo
'/'
) - 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.
Estos métodos se corresponden con los necesarios en aplicaciones CRUD.
CRUD | Verbo HTTP |
---|---|
Create | post |
Read | get |
Update | put |
Delete | delete |
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 comoJSON
)
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.
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/
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'));
/public
al directorio desde el cual servimos los estáticos.
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()`
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.
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');
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.
- Ver Basic routing
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;
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;
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
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.
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
[EN DESARROLLO]
Es importante notar que los parámetros recibidos por URL son siempre strings
.
[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.
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
yResponse
- 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
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.
En el caso de utilizar un middleware externo (a través de NPM
), debemos seguir los siguientes pasos
- Instalarlo (ej:
npm i body-parser
) - Importarlo (ej:
const bodyParser = require('body-parser')
) - 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
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.
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
[EN DESARROLLO]
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
.
- Crear un archivo
.env
. En este archivo vamos a setear las variables de entorno de nuestro proyecto. - Instalar y utilizar
nodemon
para correr nuestras aplicaciones, con el scriptdev
delpackage.json
- 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;
.env
no debe comitearse, agregarlo al .gitignore
-
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 puerto8001
. Cuando el servidor esté levantado y corriendo, la aplicación debe loguear por consolaExpress app listening on port ${PORT}!
, dondePORT
es el puerto seteado. -
Modificar el ejercicio anterior, para que al recibir un request
GET
a la ruta/salute/{name}
, la aplicación responda con un'Hola {name}!'
, dondename
es un parámetro que recibe por URL. -
Rehacer el ejercicio 2 utilizando Express. Usar
pug
como template engine para retornar las diferentes vistas HTML de nuestra aplicación. -
Rehacer el ejercicio 6, sirviendo los archivos estáticos (assets) con
Express
desde la carpeta/public
. -
Ver express-salad