====== Desarrollo de aplicaciones: backend + frontend ======
{{ frontendbackend.jpg }} Frontend vs Backend (Fuente: https://ecdisis.com/que-es-frontend-y-backend/)
===== Configuración del proyecto ===== Crear un fichero [[https://docs.npmjs.com/creating-a-package-json-file|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: * ''npm init'': Crear un fichero ''package.json'' de forma interactiva (si se añade ''--yes'' se rellenará directamente con valores por defecto) * ''npm run'': Lista los scripts disponibles * ''npm run [[https://docs.npmjs.com/cli/v10/configuring-npm/package-json|Especificación completa para el fichero package.json]] ==== Usando nodemon ==== [[https://www.npmjs.com/package/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? ====
{{ web_api.png }} Web API (Fuente: https://www.programmingwithmukesh.com/articles/web-api)
{{ urls.png }} 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:
{{ simple_structure.png }} Estructura proyecto cities-backend
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: * http://localhost:8080/cities: Para ver el listado de todas las ciudades en formato JSON
{{ sample1-cities.png }} Listado de todas las ciudades
* http://localhost:8080/city/Zaragoza: Para ver los datos de una ciudad concreta (en este caso Zaragoza pero podríamos indicar cualquiera de las existentes)
{{ sample1-city.png }} Detalles de una ciudad concreta
También podemos utilizar aplicaciones como [[https://hoppscotch.com/|Hoppscotch]] que son específicas para desarrolladores. Son clientes diseñados para realizar llamadas a APIs y testear que éstas funcionan correctamente.
{{ hoppscotch.png }} Hoppscotch
===== Frontend ===== Para el desarrollo de un frontend, desarrollaremos otra aplicación con su propio ''package.json'' y las dependencias que necesitaremos: { "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: Ejemplo 1: API con Node.js

Ejemplo de API con Node.js

Y este será nuestro código para llamar al backend, conseguir los datos y mostrarlos en el documento HTML anterior: 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)
{{ cities-frontend.png }} 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 [[https://en.wikipedia.org/wiki/Create,_read,_update_and_delete|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 [[https://entornos-desarrollo.codeandcoke.com/_detail/apuntes:urls.png?id=apuntes%3Abackend_frontend|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:
{{ be_fe_structure.png }} 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: 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: Ejemplo 1: API con Node.js

Ejemplo de API con Node.js


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: 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)
{{ cities-frontend-crud.png }} cities-frontend-crud
===== Organización de un proyecto =====
{{ nodejs_api_structure.png }} 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: * **route**: Se definen las rutas o endpoints disponibles para cada uno de los elementos del modelo de datos * **controller**: Se definen las funciones que reciben las peticiones de las rutas definidas en la capa ''route'' * **service**: Funciones que definen la lógica de aplicación 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 =====
{{ api_crud_structure.png }} 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'). 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. 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//: 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: 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): 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 [[https://github.com/codeandcoke/nodejs|nodejs]] de GitHub. * **cities-backend**: API sencilla. Sólo backend. Sin base de datos * **cities-backend-db**: API con CRUD completo. Sólo backend. Los datos están almacenados en una base de datos SQLite * **cities-backend-db-routes**: API con CRUD completo. Sólo backend. Los datos están almacenados en una base de datos SQLite. El código está organizado por capas (route, controller, service) * **cities-backend-frontend**: API sencilla. Incluye un frontend HTML + Javascript con llamadas al backend usando axios. Sin base de datos * **cities-backend-frontend-crud**: API sencilla con operación de registro. Incluye un frontend HTML + Javascript con llamadas al backend usando axios. Sin base de datos * **cities_collection.json**: Colección de requests para usar con Hoppscotch y cualquiera de los proyectos cities-backend ---- (c) 2024 Santiago Faci