Table of Contents

Desarrollo de aplicaciones: backend + frontend

frontendbackend.jpg
Figure 1: Frontend vs Backend (Fuente: https://ecdisis.com/que-es-frontend-y-backend/)

Configuración del proyecto

Crear un fichero package.json:

{
  "name": "cities-backend",
  "version": "0.1",
  "description": "A sample project to learn Node.js",
  "scripts": {
    "start": "node src/app.js"
  },
  "author": "Santiago Faci",
  "license": "GPL-2.0-only",
 
  "dependencies": {
    "express": "^4.21.1"
  }
}

Algunos comandos útiles:

Usando nodemon

nodemon es una herramienta que permite que nuestra aplicación nodejs se reinicie automáticamente cada vez que detecta un cambio en el código. De esa manera podemos evitar tener que estar continuamente parando y arrancándola.

Primero tendremos que instalar nodemon de forma global (de forma que podamos ejecutarlo como un nuevo comando de nuestro equipo):

santi@localhost:$ npm install -g nodemon

A partir de entonces, si tenemos bien configurado el script start de nuestro proyecto en el fichero package.json, podremos lanzar el proyecto simplemente ejecutando nodemon y éste monitorizará nuestro código en busca de cambios para reiniciarlo automáticamente. Asi podemos centrarnos simplemente en escribir código y comprobar si éste funciona correctamente:

santi@localhost:$ nodemon
[nodemon] 3.1.7
[nodemon] to restart at any time, enter `rs`
[nodemon] watching path(s): *.*
[nodemon] watching extensions: js,mjs,cjs,json
[nodemon] starting `node src/app.js`
Iniciando el backend en el puerto 8080

Gestión de dependencias

Añadir las dependencias necesarias para el proyecto al fichero package.json en la sección dependencies (se pueden ir añadiendo a medida que son necesarias durante el desarrollo del proyecto):

. . .
"dependencies": {
    "express": "^4.21.1"
  }
. . .

Instalar todas las dependencias necesarias para el proyecto:

santi@localhost:$ npm install

Actualizar las dependencias necesarias para el proyecto, si es necesario:

santi@localhost:$ npm update

También se pueden instalar una dependencia concreta y ésta será añadida automáticamente al fichero package.json como tal:

santi@localhost:$ npm install express

Backend

¿Qué es una API?

Figure 2: Web API (Fuente: https://www.programmingwithmukesh.com/articles/web-api)
Figure 3: Tipos de operaciones en una Web API

API sencilla sólo lectura

En un proyecto sencillo como este donde solamente vamos a desarrollar un backend sencillo, la estructura quedará algo asi:

Figure 4: Estructura proyecto cities-backend
src/app.js
const express = require('express');
 
const app = express();
app.use(express.json());
 
const cities = {
    'Zaragoza': {
        altitude: 199,
        population: 673010
    },
    'Huesca': {
        altitude: 488,
        population: 53305
    },
    'Teruel': {
        altitude: 915,
        population: 25900
    }
};
 
app.get('/cities', (req, res) => {
    res.json(cities);
});
 
app.get('/cities/:city', (req, res) => {
    const city = req.params.city;
    res.json(cities[city]);
});
 
 
app.listen(8080, () => {
    console.log('Iniciando el backend en el puerto 8080');
});

Asumiendo que tenemos un fichero package.json como el explicado anteriormente, podremos ejecutar la API con el siguiente comando:

santi@localhost:$ nodemon

Y tendremos los siguiente endpoints disponibles:

Figure 5: Listado de todas las ciudades
Figure 6: Detalles de una ciudad concreta

También podemos utilizar aplicaciones como Hoppscotch que son específicas para desarrolladores. Son clientes diseñados para realizar llamadas a APIs y testear que éstas funcionan correctamente.

Figure 7: Hoppscotch

Frontend

Para el desarrollo de un frontend, desarrollaremos otra aplicación con su propio package.json y las dependencias que necesitaremos:

package.json
{
    "name": "cities-frontend",
    "version": "0.1",
    "source": "src/index.html",
    "type": "module",
    "scripts": {
        "start": "parcel --no-cache",
        "build": "parcel build"
    },
    "devDependencies": {
        "buffer": "^6.0.3",
        "parcel": "latest",
        "process": "^0.11.10"
    },
    "dependencies": {
        "axios": "^1.7.7"
    }
}

Nuestro cliente estará basado, por el momento, en una única página HTML donde mostraremos los datos:

index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Ejemplo 1: API con Node.js</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
        <script type="module" src="index.js"></script>
    </head>
    <body onload="readCities()">
        <div class="container">
            <h1>Ejemplo de API con Node.js</h1>
            <img src="nodejs.png"/>
            <ul id="cities" class="list-group">
            </ul>
        </div>
    </body>
</html>

Y este será nuestro código para llamar al backend, conseguir los datos y mostrarlos en el documento HTML anterior:

index.js
import axios from 'axios';
 
function addCityNode(name) {
    const citiesUl = document.getElementById('cities');
 
    const item = document.createElement('li');
    item.className = 'list-group-item';
    item.appendChild(document.createTextNode(name));
    citiesUl.appendChild(item);
};
 
window.readCities = function() {
    axios.get('http://localhost:8080/cities')
        .then((response) => {
            const cityList = response.data;
 
            Object.keys(cityList).forEach(cityName => {
                addCityNode(cityName);
            });
        });
};

Tal y como hacemos con el backend, podremos ejecutar este frontend con el siguiente comando:

santi@localhost:$ npm start

Hay que tener en cuenta que backend y frontend deben escuchar en puertos diferentes para poder ejecutarse al mismo tiempo. En nuestro caso el backend escucha en el puerto 8080 y el frontend en el 1234 (es el puerto por defecto en el que escuchará porque estamos usando parcel para lanzarlo)

Figure 8: cities-frontend

CRUD Backend + Frontend

Una vez que hemos visto una sencilla API con unicamente un par de operaciones de lectura, vamos a ver un ejemplo algo más completo. Es lo que también se conoce como CRUD que es el acrónimo que se utiliza para referirse a las 4 operaciones básica que permite que los usuarios puedan registrar (Create), leer (Read), modificar (Update) y borrar (Delete) información utilizando algún tipo de software.

En el caso de la API (backend) deberán ser diseñadas conforme a las convenciones que vimos un poco antes cuando explicamos lo que era una API.

En este caso necesitamos dos proyectos separados: backend y frontend. Asi, tendremos ambos lados de la aplicación (servidor y cliente) totalmente separados. La estructura del proyecto completo será algo asi:

Figure 9: Estructura proyecto backend + frontend

Backend

Vamos a ampliar nuestro backendo con una nueva operación POST que permite añadir nuevas ciudades al listado que tenemos:

backend/src/app.js
const express = require('express');
const cors = require('cors');
 
const app = express();
app.use(cors());
app.use(express.json());
 
const cities = {
    'Zaragoza': {
        altitude: 199,
        population: 673010
    },
    'Huesca': {
        altitude: 488,
        population: 53305
    },
    'Teruel': {
        altitude: 915,
        population: 25900
    }
};
 
app.get('/cities', (req, res) => {
    res.json(cities);
});
 
app.get('/cities/:city', (req, res) => {
    const city = req.params.city;
    res.json(cities[city]);
});
 
app.post('/cities', (req, res) => {
    const name = req.body.name;
    const altitudeValue = req.body.altitude;
    const populationValue = req.body.population;
 
    cities[name] = {
        altitude: altitudeValue,
        population: populationValue
    };
    console.log(cities);
    res.status(201).end();
});
 
app.listen(8080, () => {
    console.log('Iniciando el backend en el puerto 8080');
});

Frontend

Y en el lado frontend añadiremos un formulario que permita introducir los datos necesarios para luego añadir esa ciudad y sus detalles a la lista que tenemos:

frontend/src/index.html
<!DOCTYPE html>
<html>
    <head>
        <title>Ejemplo 1: API con Node.js</title>
        <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" crossorigin="anonymous">
        <script type="module" src="index.js"></script>
    </head>
    <body onload="readCities()">
        <div class="container">
            <h1>Ejemplo de API con Node.js</h1>
            <img src="nodejs.png"/>
            <ul id="cities" class="list-group">
            </ul>
            <hr/>
            <form>
                <div class="mb-3">
                    <label for="name" class="form-label">Nombre</label>
                    <input class="form-control" type="text" id="name"/>
                </div>
                <div class="mb-3">
                    <label for="altitude" class="form-label">Altitud</label>
                    <input class="form-control" type="text" id="altitude"/>
                </div>
                <div class="mb-3">
                    <label for="population" class="form-label">Población</label>
                    <input class="form-control" type="text" id="population"/>
                </div>
                <button type="button" class="btn btn-primary" onClick="addCity()">Add city</button>
            </form>
        </div>
    </body>
</html>

En la lógica del frontend añadimos lo necesario para recoger los datos del formulario e invocar al backend para “registrar” la ciudad con sus detalles:

frontend/src/index.js
import axios from 'axios';
 
function addCityNode(name) {
    const citiesUl = document.getElementById('cities');
 
    const item = document.createElement('li');
    item.className = 'list-group-item';
    item.appendChild(document.createTextNode(name));
 
    const button = document.createElement('button');
    button.className = 'btn-close'
    button.onclick = function() {
        removeCity(name);
        item.remove();
    };
    item.appendChild(button);
 
    citiesUl.appendChild(item);
};
 
window.readCities = function() {
    axios.get('http://localhost:8080/cities')
        .then((response) => {
            const cityList = response.data;
 
            Object.keys(cityList).forEach(cityName => {
                addCityNode(cityName);
            });
        });
};
 
window.addCity = function() {
    const name = document.getElementById('name').value;
    const altitude = document.getElementById('altitude').value;
    const population = document.getElementById('population').value;
 
    if (name === '') {
        alert('El nombre es un campo obligatorio');
        return;
    }
 
    axios.post('http://localhost:8080/cities', {
        name: name,
        altitude: altitude,
        population: population
    }).then(() => {
        addCityNode(name);
    });
};
 
window.removeCity = function(name) {
    console.log(name + ' was removed');
    // TODO Remove the city
};

En el frontend veremos que también hemos añadido una X a modo de botón al lado de cada ciudad. Como ejercicio nos queda pendiente hacer que al pulsar en ese botón, la ciudad se elimine (también en el backend)

Figure 10: cities-frontend-crud

Organización de un proyecto

Figure 11: Arquitectura API Node.js (Fuente: https://www.coreycleary.me/project-structure-for-an-express-rest-api-when-there-is-no-standard-way)

En nuestro caso trabajaremos con las siguientes capas:

A continuación desarrollaremos una API completa (CRUD) utilizando una base de datos y con el código organizado correctamente siguiendo el modelo de capas que se ha planteado anteriormente.

API con CRUD completo y base de datos

Figure 12: Estructura proyecto Node.js

Backend: API

El primer paso será preparar el fichero de configuración de la base de datos, indicando el tipo (SQLite) y la ubicación (el fichero 'cities.db').

src/configuration/database.js
const knex = require('knex');
 
// Configuración de la base de datos: tipo, ubicación y otros parámetros
const db = knex({
    client: 'sqlite3',
    connection: {
        filename: 'cities.db'
    },
    useNullAsDefault: true
});
 
exports.db = db;

En el fichero principal simplemente añadiremos las operaciones definidas en el fichero 'route/cities.js' e iniciaremos el backend.

src/app.js
const express = require('express');
 
const cities = require('./route/cities.js');
 
const app = express();
app.use(express.json());
 
app.use('/', cities);
 
app.listen(8080, () => {
    console.log('Iniciando el backend en el puerto 8080');
});

El fichero 'routes/cities.js' contiene la lista de todas las operaciones definidas en este backend y que tendremos que implementar correctamente en la capa controller:

src/route/cities.js
const express = require('express');
const router = express.Router();
 
const { getCities, getCity, postCity, putCity, deleteCity } = require('../controller/cities.js');
 
router.get('/cities', getCities);
router.get('/cities/:city', getCity);
router.post('/cities', postCity);
router.put('/cities/:city', putCity);
router.delete('/cities/:city', deleteCity);
 
module.exports = router;

La capa controller implementa todas las operaciones que se han definido en la capa routes invocando a la capa service que será donde desarrollaremos toda la lógica. En esta capa controller simplemente leeremos la petición (que puede tener o no algún parámetro), validaremos dicha petición y sus parámetros e invocaremos a la capa service para ejecutar toda la lógica asociada a la misma:

src/controller/cities.js
const { findAllCities, findCity, registerCity, modifyCity, removeCity} = require('../service/cities.js');
 
// Operación que devuelve todas las ciudades de la base de datos
const getCities = (async (req, res) => {
    const data = await findAllCities();
 
    res.status(200).json(data);
});
 
// Operación que devuelve una ciudad determinada
const getCity = (async (req, res) => {
    const data = await findCity(req.params.city);
 
    res.status(200).json(data);
});
 
// Operación que registra una nueva ciudad en la base de datos
const postCity = (async (req, res) => {
    await registerCity(req.body.name, req.body.population, req.body.altitude);
 
    res.status(201).json({});
});
 
// Operación que modifica una ciudad en la base de datos
const putCity = (async (req, res) => {
    await modifyCity(req.params.city, req.body.population, req.body.altitude);
 
    res.status(204).json({});
});
 
// Operación que elimina una ciudad de la base de datos
const deleteCity = (async (req, res) => {
    await removeCity(req.params.city);
 
    res.status(204).json({})
});
 
module.exports = {
    getCities,
    getCity,
    postCity,
    putCity,
    deleteCity,
};

En nuestro caso, en la capa service, será donde accedamos a la base de datos a por la información requerida, hagamos la operaciones de lógica necesarias (si las hay) y devolveremos la información a la capa anterior (controller) para que ésta la devuelva a su superior (que será quien haya realizado la petición, el frontend):

src/service/cities.js
const db = require('../configuration/database.js').db;
 
// Operación que devuelve todas las ciudades de la base de datos
const findAllCities = (async () => {
    const result = await db('cities').select('*');
    return result;
});
 
// Operación que devuelve una ciudad determinada
const findCity = (async (cityName) => {
    const result = await db('cities').select('*').where({name: cityName}).first();
    return result;
});
 
// Operación que registra una nueva ciudad en la base de datos
const registerCity = (async (cityName, cityPopulation, cityAltitude) => {
    const result = await db('cities').insert({
        name: cityName,
        population: cityPopulation,
        altitude: cityAltitude
    });
 
    return result;
});
 
// Operación que modifica una ciudad en la base de datos
const modifyCity = (async (cityName, cityPopulation, cityAltitude) => {
    const result = await db('cities').where({ name: cityName }).update({
        population: cityPopulation,
        altitude: cityAltitude
    });
 
    return result;
});
 
// Operación que elimina una ciudad de la base de datos
const removeCity = (async (cityName) => {
    const result = await db('cities').del().where({name: cityName});
 
    return result;
});
 
module.exports = {
    findAllCities,
    findCity,
    registerCity,
    modifyCity,
    removeCity,
};

Proyectos de ejemplo

Todos los proyectos de ejemplo de esta parte están en el repositorio nodejs de GitHub.


© 2024 Santiago Faci