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:
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 disponiblesnpm run <nombre_script
: Ejecuta el script cuyo nombre se para como parámetronpm start
: Ejecuta el script start
(se puede ejecutar sin añadir run
al tratarse de uno de los scripts definidos por nodejs)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
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
En un proyecto sencillo como este donde solamente vamos a desarrollar un backend sencillo, la estructura quedará algo asi:
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:
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.
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:
<!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:
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)
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:
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'); });
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:
<!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:
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)
En nuestro caso trabajaremos con las siguientes capas:
route
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.
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, };
Todos los proyectos de ejemplo de esta parte están en el repositorio nodejs de GitHub.
© 2024 Santiago Faci