En este artículo nos centraremos en mejorar y organizar nuestro código, ya que a medida que aumente el número de endpoints de nuestra API, se volverá difícil mantenerlo limpio y organizado.
En nuestro ejemplo anterior sólo creado dos endpoints, pero podría haber muchos más. De hecho, lo normal suele ser que tengamos muchas más rutas en nuestra aplicación. Para organizar el código y evitar que se haga difícil de mantener podemos crear controladores.
¿Qué es un controlador?
Un controlador (controller) no es más que una función que se encarga de controlar la lógica de ese endpoint o ese grupo de endpoints. Dependiendo del caso, esto se podría hacer mediante una clase (Programación orientada a objetos), mediante funciones (Programación funcional), siempre idealmente colocándolo en un archivo diferente para tenerlo todo mejor organizado.
Vamos a tomar el ejemplo final del último artículo y vamos a tomarnos un tiempo en reorganizarlo un poco, por lo que si no lo has visto aún, echale un vistazo antes de seguir.
Empezaremos por sacar a un fichero los datos de usuario. Podríamos colocarlos en un fichero .json
e importarlos con JSON modules, pero de momento lo haremos con un fichero data.js
, que tiene mejor soporte:
// index.js
import { data } from "./data.js";
/* ... */
// data.js
export const data = [
{ "name": "ManzDev", "role": "streamer", "likes": ["css", "js", "webcomponents" ] },
/* ... */
];
Esto mantendrá nuestros datos aislados de la aplicación. En el futuro estarán en una base de datos, pero de momento los tenemos así para simplificarnos el ejemplo.
Crear controladores
Para simplificar lo máximo posible nuestro código, lo que vamos a hacer es crear una carpeta /controllers
donde vamos a colocar uno o varios ficheros que se encargarán de controlar la lógica de las rutas. De esta forma será mucho más sencillo acceder al código de cada endpoint, modificarlo y extenderlo.
Usando funciones
Una primera forma de abordarlo, sería separando en un fichero por cada función que controla un endpoint, es decir, por cada controlador. Nuestro fichero principal quedaría así:
// index.js
import { getUserByName } from "./controllers/getUserByName.js";
import { getUsers } from "./controllers/getUsers.js";
import express from "express";
const app = express();
const PORT = 4321;
app.get("/users", getUserByName);
app.get("/api/user/:name", getUsers);
app.listen(PORT, () => {
console.log(`Server running at http://localhost:${PORT}/`);
});
Observa que los controladores tienen el mismo nombre que los endpoints para que sean sencillos de encontrar. Luego, en el app.get()
observa que colocamos simplemente la referencia a la función importada. Los parámetros de la función bastará con indicarlos en cada fichero de controlador.
A continuación tienes los dos ficheros de controladores de la carpeta controllers
:
// controllers/getUserByName.js
import { data } from "./data.js";
export const getUserByName = (req, res) => {
res.header("Content-Type", "text/html");
res.write("Lista de usuarios:");
const userList = data.map(user => user.name);
res.write(userList.join(", "));
}
// controllers/getUsers.js
import { data } from "./data.js";
export const getUsers = (req, res) => {
const name = req.params.name;
const user = data.find(user => user.name === name);
if (user) {
res.json(user);
}
else {
res.status(404).send({ error: `No existe el usuario ${name}` });
}
}
Usando una clase (OOP)
En este caso hemos optado por utilizar funciones separadas en archivo por simplicidad, pero otra opción interesante podría ser usar programación orientada a objetos para crear una clase con todos los métodos relacionados. En ese caso, el código del fichero principal sería el siguiente:
// index.js
import { users } from "./controllers/Users.js";
/* ... */
app.get("/users", users.getUserByName);
app.get("/api/user/:name", users.getUsers);
Observa que sólo importamos un fichero, que contiene una clase con los métodos controladores. Veamos ahora el contenido de ese controlador /controllers/Users.js
:
// controllers/Users.js
import { data } from "./data.js";
class Users {
getUsers(req, res) { /* ... */ }
getUserByName(req, res) { /* ... */ }
}
export const users = new Users();
La ventaja de esta opción es que tenemos todos los métodos relacionados en una estructura de tipo clase. Es muy fácil de ampliar y extender. Observa que al final lo que exportamos es un objeto con la propiedad users
que contiene una instancia de la clase Users
, de modo que no tengamos que hacerlo fuera.