DESARROLLO WEB EN ENTORNO SERVIDOR
Programación de servicios Web APIs, BIBLIOTECAS DE UTILIDAD.
- 1. Introducción
- 2. APIs REST
- 3. BIBLIOTECAS
- 4. Referencias
En este tema veremos con trabajar con
- APIs REST
Y tambien veremos cómo añadir nuevas funcionalidades a nuestra aplicación web. En concreto, se estudiarán las siguientes funcionalidades:
- Datos ficticios
- Datos de archivos CSV
- Gráficos
- Creación de PDFs
- Envío de correos
- Pagos por internet
- Mapas
- Editor WYSIWYG
- Coloreado de código
Una de las funcionalidades más habituales que puede proporcionar un backend desde el lado servidor es la puesta a disposición de información, normalmente en formato JSON y extraida de alguna base de datos, destinada a ser consumida por diversos clientes.
Esta funcionalidad se proporciona mediante una API de tipo REST.
En NextJS para la creación de este tipo de APIs hacemos uso de manejadores de rutas o route handlers.
Los controladores de ruta permiten crear controladores de solicitudes personalizados para una ruta determinada mediante las APIs web Request y Response.
Los controladores de ruta sólo están disponibles dentro del directorio app. A menudo se suelen utilizar para la creación de APIs REST.
Los controladores de ruta se definen en un archivo route.js|ts dentro del directorio app:
// app/api/route.ts
export async function GET(request: Request) {}Note
No puede haber un archivo route.js en el mismo nivel de segmento de ruta que page.js.
Los siguientes métodos HTTP son compatibles: GET, POST, PUT, PATCH, DELETE, HEAD, y OPTIONS. Si se llama a un método no compatible, Next.js devolverá una respuesta 405 Method Not Allowed.
Referencias:
- https://nextjs.org/docs/app/building-your-application/routing/route-handlers
- https://nextjs.org/docs/app/api-reference/functions/next-request
- https://nextjs.org/docs/app/api-reference/functions/next-response
Los parámetros de URL o URL Parameters son partes de la URL en las cuales los valores que aparecen pueden variar de una petición a otra, aunque la estructura de la URL se mantiene.
En los route handlers también podemos acceder a los 2 tipos que existen:
- Parámetros de ruta
Path Parameters - Parámetros de consulta
Query ParametersoQuery Strings
Si tenemos la siguiente ruta:
http://localhost:3000/api/products/bristol/books?sort=author&skip=1
Y el siguiente código en src/app/api/products/[store]/[category]/route.js
export async function GET(request, { params }) {
console.log( request.nextUrl.origin )
console.log( request.nextUrl.pathname )
// Params
const { store, category } = await params
console.log( store )
console.log( category )
// SearchParams
const sort = request.nextUrl.searchParams.get("sort")
const skip = request.nextUrl.searchParams.get("skip")
console.log( sort )
console.log( skip )
// ...
}Producirá la siguiente salida:
http://localhost:3000
/api/products/bristol/books
bristol
books
author
1
Como se ha comentado anteriormente, la principal aplicación que tienen los route handlers (controladores de ruta) es la implentación de APIs. Suele ser habitual el intercambio de información mediante el formato JSON.
A continuación tienes 2 proyectos donde se implementa una API sencilla de este tipo. En ambos casos, los endpoints disponibles son:
/api/usersMétodos GET y POST/api/users/[id]Métodos GET, PUT y DELETE
Estructura de archivos en app
app
├── api
│ └── users
│ ├── [id]
│ │ └── route.js
│ └── route.js
├── favicon.ico
├── globals.css
├── layout.js
└── page.js
A continuación se muestran 2 ejemplos sencillos.
Estructura de archivos en src
.
├── app
│ ├── api
│ │ └── users
│ │ ├── [id]
│ │ │ └── route.js
│ │ └── route.js
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.js
│ └── page.js
└── lib
└── users.js
Gestión de datos en archivo lib/users.js.
Estructura de archivos en src
.
├── app
│ ├── api
│ │ └── users
│ │ ├── [id]
│ │ │ └── route.js
│ │ └── route.js
│ ├── favicon.ico
│ ├── globals.css
│ ├── layout.js
│ └── page.js
└── lib
└── mongodb.js
Gestión de datos en archivo lib/mongodb.js.
Para comprobar el correcto funcionamiento de la API, tenemos muchas herramientas. Las más conocidas son:
Una herramienta muy interesante es HTTPie, en especial su CLI, debido a su accesibilidad e inmediatez.
# Ejemplos
http -b GET https://nxapi-mongodb.vercel.app/api/users
http -b POST https://nxapi-mongodb.vercel.app/api/users nombre='Jose' edad=22
http -b PUT https://nxapi-mongodb.vercel.app/api/users/67ad04ef7152afc8f3d0d1f9 nombre='José' edad=23
http -b DELETE https://nxapi-mongodb.vercel.app/api/users/67ad04ef7152afc8f3d0d1f9Aunque también dispone de su versión GUI
El intercambio de recursos entre orígenes -Cross-Origin Resource Sharing (CORS)- es una característica de seguridad que te permite controlar qué sitios pueden acceder a tus recursos. Puedes configurar el encabezado Access-Control-Allow-Origin para permitir que un origen específico acceda a sus controladores de ruta.
En NextJS existen 2 formas sencillas de habilitar CORS:
- Usando la API Web estándar para un controlador de ruta
- Agregando encabezados CORS a múltiples controladores de ruta
A continuación se muestra como se haría de la primera forma:
export async function GET(request: Request) {
return new Response('Hello, Next.js!', {
status: 200,
headers: {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Methods': 'GET, POST, PUT, DELETE, OPTIONS',
'Access-Control-Allow-Headers': 'Content-Type, Authorization',
},
})
}Para la segunda forma, deberemos editar el archivo next.config.mjs. Esta última forma nos permite establecer CORS para múltiples rutas. Por ejemplo:
const nextConfig = {
async headers() {
return [
{
source: "/api/(.*)",
headers: [
{
key: "Access-Control-Allow-Origin",
value: "*",
},
{
key: "Access-Control-Allow-Methods",
value: "GET, POST, PUT, DELETE, OPTIONS",
},
{
key: "Access-Control-Allow-Headers",
value: "Content-Type, Authorization",
},
],
},
];
}
} Referencia:
- CORS: Qué es y cómo funciona el Cross-Origin Resource Sharing
- Una tercera forma de habilitar CORS usando Middleware
Para los usuarios o aplicaciones cliente que consumen nuestra API es muy importante que ésta esté documentada.
Un sistema de documentación muy usado actualmente es OpenAPI + Swagger.
OpenAPI es una especificación estándar para describir APIs REST.
En pocas palabras, OpenAPI define cómo documentar una API de forma estructurada y legible tanto para humanos como para máquinas.
Con OpenAPI puedes describir:
- Rutas (endpoints) disponibles
- Métodos HTTP (GET, POST, PUT, DELETE, etc.)
- Parámetros de entrada
- Cuerpos de las peticiones
- Respuestas posibles
- Códigos de estado
- Autenticación (JWT, OAuth, API keys, etc.)
Se suele escribir en YAML o JSON.
Por otro lado, Swagger es un conjunto de herramientas que usan la especificación OpenAPI.
Herramientas más conocidas de Swagger:
- Swagger UI → Interfaz web para ver y probar la API
- Swagger Editor → Editor para escribir archivos OpenAPI
- Swagger Codegen → Genera código cliente o servidor automáticamente
- Swagger Hub → Plataforma colaborativa para APIs
Con Swagger UI puedes:
- Ver la documentación de la API
- Probar endpoints directamente desde el navegador
- Enviar requests sin usar Postman
Proyectos de ejemplo
A continuación tienes el código fuente de dos proyectos que implementan una API y ofrecen su documentación asociada:
En ambos casos disponemos de los 2 elementos siguientes:
- Especificación OpenAPI 3.0 ( en archivo
public/openapi.json) - Página interactiva ( en archivo
src/app/apidoc/page.js)
Especificación OpenAPI 3.0
public/openapi.json
Es un archivo JSON público que describe la API. Está organizado de la siguiente manera:
"openapi": "3.0.0",
"info": {
"title": "Shop API Documentation",
"description": "A modern REST API built with Next.js",
"version": "1.0.0"
},
"components": {
...
}
"paths": {
...
} En el apartado de components tenemos la configuración de autenticación (en este caso Bearer JWT) y los schemas (modelos de datos)
"components": {
"securitySchemes": {
"bearerAuth": {
"type": "http",
"scheme": "bearer",
"bearerFormat": "JWT"
}
},
"schemas": {
"Product": {
"type": "object",
"properties": {
"id": {
"type": "string",
"example": "cd533345-f1f3-48c9-a62e-7dc2da50c8f8",
"description": "Product ID",
"uniqueItems": true
},
"title": {
"type": "string",
"example": "T-Shirt Teslo",
"description": "Product Title",
"uniqueItems": true
},
"price": {
"type": "number",
"example": 0,
"description": "Product price"
},
"description": {
"type": "string",
"example": "Anim reprehenderit nulla in anim mollit minim irure commodo.",
"description": "Product description",
"default": null
}
},
"required": [
"id",
"title",
"price",
"description",
"slug",
"stock",
"sizes",
"gender",
"tags",
"images"
]
},
...
}
}En el apartado de paths tenemos la documentados los endpoints disponibles, cada uno con su configuración de seguridad (en caso de ser necesaria), los parameters, requestBody y responses admitidos.
"paths": {
"/api/products": {
"post": {
"summary": "Create new product",
"tags": [
"Products"
],
"security": [
{
"bearerAuth": []
}
],
"parameters": [],
"requestBody": {
"required": true,
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/ProductCreate"
}
}
}
},
"responses": {
"201": {
"description": "Product was created",
"content": {
"application/json": {
"schema": {
"$ref": "#/components/schemas/Product"
}
}
}
},
"400": {
"description": "Missing data"
},
"403": {
"description": "Forbidden. Token related."
},
"500": {
"description": "Internal server error"
}
}
},
"get": {
"summary": "List products",
"tags": [
"Products"
],
"parameters": [
{
"name": "limit",
"required": true,
"in": "query",
"description": "How many rows do you need",
"schema": {
"default": 10,
"type": "number"
}
},
{
"name": "offset",
"required": true,
"in": "query",
"description": "How many rows do you want to skip",
"schema": {
"default": 0,
"type": "number"
}
}
],
"responses": {
"200": {
"description": "Product list"
}
}
}
},
...
}Tip
Es posible escribir la especificación en formato YAML, que es menos verboso que JSON.
Página interactiva
src/app/api/page.js
En este proyecto se ha utilizado un sistema de Autorización mediante Bearer token, siendo éste de tipo JWT.
Los endpoints que modifican la información de la API (métodos POST, PATCH, PUT y DELETE) requieren que el cliente proporcione el JWT que previamente habrá solicitado al servidor. Este token estará firmado con la clave secreta del servidor y su autenticidad puede comprobarse en jwt.io
Aquí se exponen someramente algunas de las bibliotecas que proporcionan funcionalidades que en ciertas circunstancias pueden ser de nuestro interés.
Durante el desarrollo de una aplicación a menudo necesitamos datos fake para probar su funcionalidad. Para ello disponemos de la biblioteca faker.js, la cual nos provee de este tipo de datos.
Esta biblioteca puede usarse desde el lado cliente y desde el lado servidor.
Instalamos como dependencia de desarrollo. Aparecerá en la sección devDependencies en el archivo package.json.
npm install -D @faker-js/fakerImportant
Esta biblioteca ocupa unos cuantos MB.
import { faker } from '@faker-js/faker';
// Algunos datos disponibles.
// Para ver un listado exhaustivo, consulta la API
faker.number.int()
faker.number.int({ min: 10, max: 100 })
faker.number.float()
faker.number.float({ min: 10, max: 100, multipleOf: 0.02 })
faker.person.firstName()
faker.person.firstName('female')
faker.person.firstName('male')
faker.person.lastName()
faker.person.fullName()
faker.person.bio()
faker.internet.email()
faker.internet.email({ firstName: 'Jeanne', lastName: 'Doe' }) // 'Jeanne_Doe63@yahoo.com'
faker.internet.email({ firstName: 'Jeanne', lastName: 'Doe', provider: 'example.dev' }) // 'Jeanne_Doe88@example.dev'
faker.animal.dog() // 'Irish Water Spaniel'
faker.animal.cat() // 'Singapura'
faker.animal.horse() // 'Swedish Warmblood'
faker.image.avatar()
faker.image.city()Es posible que se te presente la posibilidad de realizar una aplicación que requiera el uso de datos de poca complejidad y de sólo lectura. En estos casos es problable que usar una BD sea algo parecido a matar moscas a cañonazos.
Disponemos de una biblioteca llamada Papa Parse que nos permite almacenar nuestros datos en uno o varios archivos CSV, y realizar las consultas deseadas sobre ellos.
Esta biblioteca puede usarse desde el lado cliente y desde el lado servidor.
npm install papaparseimport Papa from 'papaparse';
Papa.parse(file, config);A continuación tienes los enlaces a 2 proyectos que hacen uso de esta biblioteca.
- Aplicación de lado cliente para rellenar fichas semanales
- Aplicación de lado servidor para consultar biblioteca de libros
La segunda aplicación contiene los datos en el archivo book.csv. CUIDADO: son unos 70MB.
Para cargar grandes cantidades de datos desde CSV a una BD consulta el siguiente vídeo:
Si tenemos un archivo localidades.csv con los datos en formato CSV, podemos convertirlos fácilmente a datos JSON con algún conversor, como csvjson.com y guardarlos en un archivo localidades.json.
Los datos CSV siguientes
"id","nombre","temp_ext_ver","hr_ext_ver","temp_ext_inv","hr_ext_inv","altitud","zona_climatica"
1,"Álava (Vitoria / Aeropuerto de Foronda)",30.1,49,-2.4,96,508,"D"
2,"Albacete (Los Llanos-Base Aérea)",34.2,25,-3,92.3,704,"D"
3,"Albacete (Hellín)",36.7,27,1.5,70.4,520,"D"se convertirán en los datos JSON siguientes
[
{
"id": 1,
"nombre": "Álava (Vitoria / Aeropuerto de Foronda)",
"temp_ext_ver": 30.1,
"hr_ext_ver": 49,
"temp_ext_inv": -2.4,
"hr_ext_inv": 96,
"altitud": 508,
"zona_climatica": "D"
},
{
"id": 2,
"nombre": "Albacete (Los Llanos-Base Aérea)",
"temp_ext_ver": 34.2,
"hr_ext_ver": 25,
"temp_ext_inv": -3,
"hr_ext_inv": 92.3,
"altitud": 704,
"zona_climatica": "D"
},
{
"id": 3,
"nombre": "Albacete (Hellín)",
"temp_ext_ver": 36.7,
"hr_ext_ver": 27,
"temp_ext_inv": 1.5,
"hr_ext_inv": 70.4,
"altitud": 520,
"zona_climatica": "D"
}
]NextJS tiene soporte nativo para datos JSON. Por ejemplo, podemos hacer
import localidades from '@/lib/localidades.json'
// ...
console.log( localidades.find ( localidad => localidad.id == 1 ) ) // Mostramos localidad con id 1De esta forma tan sencilla, nos ahorramos tener que usar papaparse, y así reducir dependencias y simplificar el desarrollo.
La funcionalidad de arrastrar y soltar es propia del lado cliente y por tanto deberá ser usada desde componentes cliente. Además de poder realizarla con Vanilla JS, también disponemos de 2 bibliotecas interesantes, que son:
A diferencia de muchas bibliotecas de arrastrar y soltar, DnD Kit no está construido intencionalmente sobre la API de arrastrar y soltar HTML5. Esta fue una decisión arquitectónica deliberada, que conlleva desventajas que debe conocer antes de decidir usarla. Para la mayoría de las aplicaciones web, creemos que los beneficios superan las desventajas.
La API de arrastrar y soltar de HTML5 tiene algunas limitaciones severas. No es compatible con dispositivos táctiles, lo que significa que las bibliotecas que se construyen sobre ella deben exponer una implementación completamente diferente para admitir dispositivos táctiles.
La principal desventaja de que DnD Kit no use la API de arrastrar y soltar de HTML5 es que no podrá arrastrar desde el escritorio o entre ventanas.
Si el caso de uso de arrastrar y soltar que tiene en mente implica este tipo de funcionalidad, definitivamente querrá usar una biblioteca que esté construida sobre la API de arrastrar y soltar de HTML5.
Le recomendamos que consulte react-dnd para obtener una biblioteca de React que tenga un backend de arrastrar y soltar de HTML5 nativo.
| Soporta pantallas táctiles | DnD desde escritorio y entre ventanas | |
|---|---|---|
| DnD Kit | ✓ | 𐄂 |
| React DnD | 𐄂 | ✓ |
npm install react react-dom @dnd-kit/core npm install react react-dom react-dnd react-dnd-html5-backend- Para DnD Kit seguir la siguiente guía: https://docs.dndkit.com/introduction/getting-started
- Para React DnD seguir el siguiente tutorial: https://react-dnd.github.io/react-dnd/docs/tutorial
Otras herramientas que nos permiten hacer DnD son:
Esta biblioteca debe ser usada desde el lado cliente.
Otra de las funcionalidades que a menudo es requerida es la de presentación de información en forma gráfica. Suele tratarse de diagramas para la exposición de datos o de monitorización de ellos (en algunos casos en tiempo real). En este último caso, los gráficos suelen estar incorporados en un Dashboard o Panel del usuario.
La web W3Schools tiene una introdución a los gráficos explicando brevemente las bibliotecas más habituales para el trabajo con gráficos.
Nosotros usaremos la biblioteca chart.js por ser una de las más populares. Además es sencilla de usar y tiene licencia MIT, la cual es muy permisiva.
Hay una demo disponible en Vercel. El código fuente está disponible en:
Note
Otra biblioteca muy interesante es plotly para javascript. Es una biblioteca originalmente desarrollada para el lenguaje Python.
npm install plotly.js-dist-minCaracterísticas:
- Mayor cantidad de gráficas de chart.js
- Mayor interactividad
- Buena integración con datos en tiempo real
A pesar de sus interesantes características, una desventaja muy importante es que su integración con NextJS es bastante mala. Sin embargo funciona muy bien con Vanilla JS.
Más documentación:
- Fundamentos
- Integración con React (
npm install react-plotly.js plotly.js)
npm install chart.js react-chartjs-2El segundo paquete (react-chartjs-2) nos permitirá usar directamente componentes de React, en lugar de Vanilla JS.
La forma más rápida y directa de empezar a usar esta biblioteca es usando el siguiente esquema.
import { Chart } from 'chart.js/auto' // versión 4 de chartjs
import { Line } from 'react-chartjs-2';
const options = {
plugins: {
title: /* ... */,
legend: /* ... */,
},
scales: /* ... */,
}
const data = {
labels: /* ... */,
datasets: [
{
label: /* ... */,
data: /* ... */,
},
{
label: /* ... */,
data: /* ... */,
}
],
};
export default function grafico() {
return (
<div className='contenedor '>
<Line options={options} data={data} />
</div>
)
}Sin embargo, para despliegue en producción, en lugar de usar import { Chart } from 'chart.js/auto' es recomendable usar la forma que aparece a continuación, puesto que reducirá el peso del código resultante. En este segundo caso deberemos importar los componentes especificos que vayamos a necesitar y además realizar un registro de ellos.
// Esto es un ejemplo para un gráfico tipo Scatter.
// Para otro tipo de gráfico necesitarás importar otros componentes.
import {
Chart,
LinearScale,
PointElement,
Tooltip,
Legend,
} from 'chart.js';
Chart.register(LinearScale, PointElement, Tooltip, Legend);Haciendo uso del paquete react-chartjs-2 importamos el tipo de gráfico deseado. En el ejemplo que viene a continuación importamos un gráfico de tipo Scatter.
Luego seguimos el siguiente procedimiento:
- Definimos un constante
optionscon la configuración general del gráfico. - Definimos una constante
datacon la configuración específica del gráfico. - Usamos el componente pasándole las propiedades anteriores.
import { Scatter } from 'react-chartjs-2';
const options = {
scales: {
y: {
beginAtZero: true,
},
},
};
const data = {
datasets: [
{
label: 'Puntos aleatorios',
data: Array.from({ length: 100 }, () => ({ // 100 puntos
x: Math.random(),
y: Math.random(),
})),
backgroundColor: 'rgba(255, 99, 132, 1)',
},
],
};
export default function grafico() {
return (
<div className='contenedor '>
<Scatter options={options} data={data} />
</div>
)
}El gráfico debe estar dentro de un elemento contenedor. El contenedor padre debe cumplir los siguientes requisitos para que el comportamiento sea responsive:
- Tener
position: relativeen su estilo CSS. - Dedicarse únicamente al lienzo del gráfico.
Chart.js usa su contenedor principal para actualizar el renderizado del lienzo (canvas) y los tamaños de visualización. Sin embargo, este método requiere que el contenedor esté relativamente posicionado y dedicado únicamente al lienzo del gráfico.
Important
La página donde vaya a incrustarse el gráfico debe contener la directiva 'use client'
En algunos casos deseamos que la gráfica muestre un flujo de datos (streaming) obtenido de alguna fuente. Para ello podemos usar el plugin chartjs-plugin-streaming. Su página web dispone de varios ejemplos.
El sitio oficial también dispone de una guía bastante detallada.
Entre toda la información podemos destacar:
El paquete chartjs-plugin-streaming soporta obtención de datos mediante los modos pull y push. Para mayor información consulta la documentación oficial
Hay disponible una aplicación de ejemplo con código fuente disponible en Github.
Ejecución en entorno de desarrollo
git clone https://github.com/jamj2000/nxchart-streaming.git
cd nxchart-streaming
npm install
npm run devEjecución en entorno de producción
Important
El grafico6 hace uso del plugin chartjs-plugin-zoom para ofrecer funcionalidad de zoom y pan. Sin embargo este plugin provoca un error en el proceso de construcción. Por tanto el comando npm run build fallará. Si deseas llevar esta aplicación a producción, elimina este plugin del archivo src/components/grafico6.js.
Resumen del proceso de desarrollo
npm install chart.js@3 react-chartjs-2@4 luxon chartjs-adapter-luxon chartjs-plugin-streaming chartjs-plugin-zoom chartjs-plugin-annotation@1 --legacy-peer-depsImportant
A fecha de Abril de 2024, este plugin de streaming no soporta chartjs v4, por tanto debemos instalar los paquetes más antiguos usando el comando anterior
Los paquetes instalados son los siguientes:
luxon: biblioteca para manejo de fechas y horaschartjs-adapter-luxon: adaptador de la biblioteca anteriorchartjs-plugin-annotation: plugin para realizar anotaciones en el gráficochartjs-plugin-streaming: plugin para hacer streamingchartjs-plugin-zoom: plugin opcional. Para hacer zoom y pan
'use client'
import { Line } from 'react-chartjs-2';
import Chart from 'chart.js/auto'; // IMPORTANTE: versión 3 de chartjs. La forma de importar es distinta
import annotationPlugin from 'chartjs-plugin-annotation';
import 'chartjs-adapter-luxon';
import ChartStreaming from 'chartjs-plugin-streaming';
const onRefresh = (chart) => {
/* ... */
};
const options = {
responsive: true,
plugins: {
title: /* ... */,
legend: /* ... */,
annotation: { annotations: {} } // necesario para ocultar gráfica al pulsar leyenda
},
scales: {
x: {
type: 'realtime',
realtime: {
duration: 20000,
refresh: 1000,
delay: 2000,
onRefresh: onRefresh
}
},
y: {
title: {
display: true,
text: 'Valores'
}
}
},
interaction: /* ... */,
}
const data = /* ... */;
function grafico() {
Chart.register(annotationPlugin, ChartStreaming);
return (
<div className='contenedor'>
<Line options={options} data={data} />
</div>
);
}
export default graficoUna forma de recuperar los datos para realizar las gráficas es mediante el uso de una cola de mensajes.
Una cola de mensajes es una forma de comunicación asíncrona entre múltiples procesos que necesitan transmitirse diferentes datos. De esta forma un sistema añade un mensaje a la cola que se persistirá hasta que otro sistema lo consuma y lo procese.
La principal ventaja de utilizar las colas de mensajes es desacoplar diferentes sistema. También pueden ser de utilidad cuando quieres procesar múltiples operaciones en lotes, de forma que evites que se produzcan picos en la utilización de tu plataforma y se pueda degradar el servicio.
Herramientas para trabajar con colas de mensajes hay muchas: RabbitMQ, Amazon SQS, algunas más complejas para flujos de eventos como Apache Kafka o Amazon Kinesis, pero realmente puedes utilizar incluso un buffer en memoria aprovechando herramientas como Redis, o incluso MySQL como se indica en la siguiente referencia (incluye vídeo).
Referencia:
No podemos dejar de nombrar al SGBD InfluxDB que es un sistema especialmente diseñado desde cero para trabajar con base de datos de series temporales.
InfluxDB ofrece muchas alternativas a la hora de recopilar datos, como puede apreciarse en la siguiente imagen:
Puedes instalar InfluxDB con Docker, y luego realizar la configuración accediendo a http://localhost:8086.
Referencias:
- Sitio oficial
- Qué es InfluxDB y primeros pasos
- Compare InfluxDB to SQL databases
- Paquete de npm para InfluxDB
- Monitorizando datos con InfluxDB, Telegraf y Grafana
- Código fuente de ejemplo
- Código fuente de ejemplo con streaming
- Componentes
- Ejemplos de react-chartjs-2
- Documentación Plugin Streaming
- Documentación Plugin Annotation
- Documentación Plugin Zoom
- Más gráficos, plugins,... - Awesome Chart.js
- Otra biblioteca de Charts
Existe un biblioteca para charting llamada roughViz. Como su nombre indica realiza la visualización de forma algo bruta. Sirve para dar un toque de originalidad en detrimento de la exactitud en la representación gráfica.
La documentación oficial nos informa de los tipos de gráficos soportados y de las propiedades que podemos emplear en ellos. Algunas de estas propiedades son las que se muestran a continuación:
Existe integración para React.
npm install react-roughviz'use client'
import {Bar, Pie } from 'react-roughviz'
// ...
<h3>Bar</h3>
<Bar
data='https://raw.githubusercontent.com/jwilber/random_data/master/flavors.csv'
labels='flavor'
values='price'
/>
<h3>Pie</h3>
<Pie
data={{
labels: ['North', 'South', 'East', 'West'],
values: [10, 5, 8, 3]
}}
title='Regions'
colors={['red', 'orange', 'blue', 'skyblue']}
roughness={8}
/>Existen numerosas bibliotecas para la realización de gráficas. Algunas de ellas son:
- Recharts
- Highcharts
- Nivo
- D3.js: Una de las más potentes. Requiere de conocimientos avanzados.
De las muchas bibliotecas disponibles para trabajar con archivos PDF, una de las más interesantes y potentes es pdf-lib.
Permite crear nuevos documentos y modificar los existentes. Permite trabajar en todos los entornos de JavaScript, no solo en Node o el navegador.
Esta biblioteca puede usarse desde el lado cliente y desde el lado servidor.
Note
Otras bibliotecas que merecen atención son las siguientes:
- jsPDF (lado cliente y servidor)
- html2pdf.js (sólo lado cliente)
- React PDF (lado cliente y servidor)
- Puppeteer (sólo lado cliente)
Un ejemplo de uso de las 3 últimas bibliotecas se muestra en este vídeo de Youtube, en inglés y nivel medio-avanzado. Su código fuente del ejemplo está disponible en Github. También hay un artículo publicado por el autor.
Las 3 bibliotecas nos permiten convertir HTML a PDF incluido html2pdf.js que nos permite convertir un árbol HTML a PDF, React PDF para renderizar un PDF y Puppeteer para generar un PDF a partir de una página en vivo.
Según la documentación oficial de Puppeteer, se trata de una biblioteca de JavaScript que proporciona una API de alto nivel para controlar Chrome o Firefox a través del protocolo DevTools o WebDriver BiDi. Puppeteer se ejecuta en la interfaz de usuario sin cabeza (sin interfaz de usuario visible) de forma predeterminada, pero se puede configurar para que se ejecute en un navegador visible ("con cabeza"). Es decir, la principal finalidad de esta biblioteca no es trabajar con PDF, aunque tiene una parte que ofrece esta funcionalidad.
Ejemplo muy básico
mkdir puppeteer
cd puppeteer
npm init -ynpm install puppeteerimport puppeteer from 'puppeteer';
const browser = await puppeteer.launch();
const page = await browser.newPage();
await page.goto('https://spacejelly.dev');
const pdf = await page.pdf({path: './example.pdf', format: 'A4', printBackground: true});
await browser.close();mkdir pdf-lib
cd pdf-lib
npm init -ynpm install pdf-libHaremos una primera prueba desde NodeJS, sin necesidad de crear un proyecto completo en NextJS. Para ello crea el archivo pdf.mjs y ejecútalo directamente con Node.
// pdf.mjs
import { PDFDocument, StandardFonts, rgb } from 'pdf-lib'
import fs from 'node:fs/promises'
async function createPDF() {
const pdfDoc = await PDFDocument.create()
const timesRomanFont = await pdfDoc.embedFont(StandardFonts.TimesRoman)
const page = pdfDoc.addPage()
const { width, height } = page.getSize()
const fontSize = 30
page.drawText('Creating PDFs in JavaScript is awesome!', {
x: 50,
y: height - 4 * fontSize,
size: fontSize,
font: timesRomanFont,
color: rgb(0, 0.53, 0.71),
})
const pdfBytes = await pdfDoc.save()
try {
await fs.writeFile('example.pdf', pdfBytes);
} catch (err) {
console.log(err);
}
}
createPDF()Note
Una vez hayas comprobado que funciona correctamente, puedes reutilizar la función anterior createPDF como server action dentro de un proyecto NextJS.
A continuación tienes los enlaces a 2 proyectos que hacen uso de esta biblioteca.
- Aplicación de lado cliente para rellenar fichas semanales
- Aplicación de tickets de supermercado -por realizar-
El envío de correos desde una aplicación NodeJS es muy sencillo si usamos el paquete nodemailer. La dificultad suele provenir de la configuración del servidor de correo y la cuenta asociada. Realizaremos el envío de correos desde el lado servidor de nuestra aplicación, usando node.js.
Note
Lo que se aplica para NodeJS, también se aplica para NextJS desde el lado servidor.
npm install nodemailerVamos a ver 2 opciones para enviar correos desde nuestra aplicación:
- Usando una cuenta de Gmail
- Usando un servidor de correo propio
Podemos usar una cuenta de Gmail para permitir que nuestra aplicación pueda enviar correos. Google ha ido modificando este tipo de soporte, cambiando la forma de realizar la configuración a lo largo del tiempo. Actualmente para configurar este tipo de funcionalidad debemos cumplir 2 requisitos:
- tener habilitada la verificación en dos pasos en dicha cuenta
- poseer una contraseña de aplicación
Una contraseña de aplicación es una contraseña especial de 16 caracteres que te da Google para permitir que aplicaciones externas (como tu app Node.js) se conecten a tu cuenta sin usar tu contraseña principal.
Los pasos a seguir para conseguir nuestro objetivo son:
Paso 1: Activar la verificación en dos pasos
Ve a: https://myaccount.google.com/security
En la sección "Cómo inicias sesión en Google", haz clic en "Verificación en dos pasos" y actívala si aún no lo está.
Paso 2: Generar una contraseña de aplicación
Una vez activada la verificación en dos pasos, ve a https://myaccount.google.com/apppasswords
Escribe un nombre para la aplicación, por ejemplo "correo"
Copia la contraseña generada de 16 caracteres (sin espacios). Ejemplo: abcd efgh ijkl mnop
Paso 3: Instalar Nodemailer
Desde tu terminal:
npm install nodemailerPaso 4: Probar código de ejemplo con contraseña de aplicación
Haremos una primera prueba desde NodeJS, sin necesidad de crear un proyecto completo en NextJS. Para ello crearemos el archivo mail.mjs y lo ejecutaremos directamente con Node.
// mail.mjs
import nodemailer from 'nodemailer'
var transporter = nodemailer.createTransport({
host: 'smtp.gmail.com',
port: 587,
secure: false, // true para 465, false para 587
auth: {
user: "jamj2000.vercel.app@gmail.com", // coloca aquí tu usuario
pass: "saogukadgqomnkzl", // La contraseña de aplicación (16 caracteres sin espacios)
}
});
// async..await no está permitido en el global scope, debemos usar un wrapper
async function sendMail() {
// send mail with defined transport object
const info = await transporter.sendMail({
from: '"José Antonio Muñoz Jiménez 👻" <jamj2000.vercel.app@gmail.com>',
to: "jamj2000@gmail.com, jamunoz@iesincagarcilaso.com, adfadcaxs@afdaadxcdf.com",
subject: "Ejemplo con Nodemailer ✔",
text: "Mensaje de prueba. Ignoralo.",
html: "<h1>Mensaje de prueba</h1><p>Ignoralo</p>"
});
console.log("Mensaje enviado: %s", info.messageId);
// Mensaje enviado: <d786aa62-4e0a-070a-47ed-0b0666549519@jamj2000.eu>
console.log('MENSAJES ACEPTADOS: ', info.accepted);
console.log('MENSAJES RECHAZADOS: ', info.rejected);
}
sendMail().catch(console.error);Note
Una vez hayas comprobado que el envío de correo funciona correctamente, puedes reutilizar la función anterior sendMail como server action dentro de un proyecto NextJS.
Otra manera más profesional de gestionar correos es usar alguno de los servidores de correo transaccional, entre ellos:
Note
Una alternativa gratuita y sencilla, máximo 200 correos mensuales, es usar la biblioteca EmailJS. Aquí tienes un vídeo de ejemplo y aquí otro vídeo.
La configuración de un correo transaccional no suele ser demasiado complicada, pero la integración con el dominio puede dar algunos problemas. Por ejemplo, en mi caso, al tener ya configurado previamente un servidor de correo con prioridad 10, tuve que dar de alta resend con prioridad 11 en gandi.
A continuación se explica como enviar correos sin necesidad de usar un servidor de correo transaccional.
Los pasos a seguir para conseguir nuestro objetivo son:
Paso 1: Registrar un dominio con algún proveedor que ofrezca además servidor de correo.
Por ejemplo, gandi ofrece servidor de correo con 2 cuentas de correo. Los dominios .eu suelen ser bastante baratos.
Paso 2: Usar los parámetros de conexión para enviar correos con nuestra aplicación.
Si lo deseas, puedes crear alias. Son cuentas de correo adicionales asociadas a la cuenta principal.
Paso 3: Instalar Nodemailer
Desde tu terminal:
npm install nodemailerPaso 4: Probar código de ejemplo con contraseña de aplicación
Haremos una primera prueba desde NodeJS, sin necesidad de crear un proyecto completo en NextJS. Para ello crea el archivo mail.mjs y ejecútalo directamente con Node.
// mail.mjs
import nodemailer from 'nodemailer'
var transporter = nodemailer.createTransport({
host: "mail.gandi.net", // coloca aquí tu servidor de correo
port: 465,
auth: {
user: "info@jamj2000.eu", // coloca aquí tu usuario
pass: "tu_contraseña", // coloca aquí tu contraseña
}
});
// async..await no está permitido en el global scope, debemos usar un wrapper
async function sendMail() {
// send mail with defined transport object
const info = await transporter.sendMail({
from: '"José Antonio Muñoz Jiménez 👻" <soporte@jamj2000.eu>',
to: "jamj2000@gmail.com, jamunoz@iesincagarcilaso.com, adfadcaxs@afdaadxcdf.com",
subject: "Ejemplo con Nodemailer ✔",
text: "Mensaje de prueba. Ignoralo.",
html: "<h1>Mensaje de prueba</h1><p>Ignoralo</p>"
});
console.log("Mensaje enviado: %s", info.messageId);
// Mensaje enviado: <d786aa62-4e0a-070a-47ed-0b0666549519@jamj2000.eu>
console.log('MENSAJES ACEPTADOS: ', info.accepted);
console.log('MENSAJES RECHAZADOS: ', info.rejected);
}
sendMail().catch(console.error);Note
Una vez hayas comprobado que el envío de correo funciona correctamente, puedes reutilizar la función anterior sendMail como server action dentro de un proyecto NextJS.
Los pagos online permiten a los usuarios comprar productos o servicios directamente desde una web o aplicación. Una de las plataformas más utilizadas es Stripe, que ofrece APIs seguras y fáciles de integrar.
En aplicaciones modernas con Next.js, Stripe se suele usar mediante rutas API para procesar pagos en el servidor y componentes en el cliente para recoger datos de pago.
Para que tu aplicación pueda hacer uso de stripe deberas registrarte previamente en Crear cuenta en Stripe https://dashboard.stripe.com/register
Y después obtener las claves y añadirlas al archivo .env:
- STRIPE_SECRET_KEY (privada)
- STRIPE_PUBLISHABLE_KEY (pública)
La clave privada no debe ser accesible al usuario final. Está destinada a su uso en el backend.
La clave pública puede usarse en el frontend y puede ser visible al usuario final. Se utiliza en caso que querer realizar checkout desde el frontend.
Tip
Puedes ver una aplicación, tanto su funcionalidad como su código, que implementa los pagos con stripe en los siguentes enlaces:
Desde tu terminal:
npm install stripeNote
También necesitarás instalar stripe CLI para probar y gestionar tu integración desde la línea de comandos
Important
Deberemos modificar el schema.prisma para añadir un campo stripeSessionId (posible nulo, único) al modelo Pedido para poder vincular el pago con el pedido:
model Pedido {
// ... campos existentes ...
stripeSessionId String? @unique
}En Stripe, cada vez que creas una sesión de pago (checkout.session) se genera un ID único (session.id).
Esto significa que:
- Cada intento de pago tiene su propia sesión
- No se reutilizan sesiones automáticamente
- Puedes identificar cada proceso de checkout de forma independiente
Además, deberemos modificar el schema.prisma para añadir un campo stripeCustomerId (posible nulo, único) al modelo User para poder vincular el pago con el usuario:
model User {
// ... campos existentes ...
stripeCustomerId String? @unique
}Flujo de pago
- En
app/carrito/checkout/page.jsel usuario pulsa en botón “Pagar con Stripe”. - Se ejecuta en el backend la acción de servidor (
lib/actions/checkout.js), encargada de crear una sesión de pago en Stripe. - El usuario es redirigido al checkout de Stripe.
- El usuario introduce datos de su tarjeta.
- Stripe procesa el pago de forma segura.
- Stripe envía el resultado del pago al endpoint
app/api/webhook.jsmediante un webhook. Si el evento escheckout.session.completed, se guarda la información del pedido en la base de datos. - Finalmente, el usuario es redirigido a una página de éxito o cancelación.
Note
El paso 2 también puede implementarse mediante una petición a app/api/checkout.js (endpoint /api/checkout), en lugar de usar una acción de servidor.
- El uso de un endpoint API es más adecuado en arquitecturas desacopladas (frontend/backend separados).
- El uso de acciones de servidor es más apropiado en aplicaciones fullstack monolíticas con Next.js.
Note
El paso 4 permite el uso de tarjetas de prueba. Entre otras:
Tarjeta visa EE.UU.
- Tarjeta válida:
4242 4242 4242 4242 - Fecha futura cualquiera
- CVC cualquiera
Tarjeta visa España
- Tarjeta válida:
4000 0072 4000 0007 - Fecha futura cualquiera
- CVC cualquiera
Tip
RESUMEN
- Frontend: inicia el proceso (botón de pago)
- Backend (Next.js): crea la sesión de Stripe
- Stripe: gestiona el pago de forma segura
- Webhook: confirma el resultado de forma fiable
- Base de datos: solo se actualiza cuando el pago está confirmado
- Vídeo: Pasarela de pago con Stripe en tu Web o App: Guía Paso a Paso -Python-
- Documentación de Stripe
- Testing Stripe
- Sripe CLI (Command Line Interface)
- El objeto Checkout Session
- Stripe elements: Web/App
- Números de tarjeta para pruebas
Si deseamos hacer uso de los Mapas de Google, podemos usar la biblioteca @googlemaps/js-api-loader.
Tip
Otra alternativa es usar GoogleMapsEmbed, componente proporcionado por Next.js
Note
Deberás obtener una API KEY de Google.
Esta biblioteca solo puede usarse desde el lado cliente.
npm install @googlemaps/js-api-loader
npm install @types/google.maps # Sólo necesario si vamos a usar Typescript.env
NEXT_PUBLIC_MAPS_API_KEY="YOUR API KEY"src/components/GoogleMaps.js
'use client';
import { useEffect, useRef } from 'react';
import { Loader } from '@googlemaps/js-api-loader';
export default function GoogleMaps() {
const mapRef = useRef(null);
useEffect(() => {
const initializeMap = async () => {
const loader = new Loader({
apiKey: process.env.NEXT_PUBLIC_MAPS_API_KEY,
version: "quarterly",
libraries: ["places"]
});
const { Map } = await loader.importLibrary('maps');
const locationInMap = {
lat: 37.57869021751064,
lng: -4.6471375471961025
};
// MARKER
const { AdvancedMarkerElement: Marker } = await loader.importLibrary("marker")
const options = {
center: locationInMap,
zoom: 10,
mapId: 'DEMO_MAP_ID',
};
const map = new Map(mapRef.current, options);
// add the marker in the map
const marker = new Marker({
map: map,
position: locationInMap,
});
};
initializeMap();
}, []);
return <div className="h-[600px]" ref={mapRef} />;
}src/app/page.js
import GoogleMaps from "@/components/GoogleMaps";
export default function Home() {
return (
<div className="mx-auto container w-8/12">
<GoogleMaps />
</div>
);
}- Video: Nextjs Google Maps Tutorial
- Video: Add Google Maps to your Next JS application - Easy way
- Código fuente del video anterior
- Documentación oficial
- Bibliotecas disponibles
Tiptap - Editor WYSIWYG ( sitio oficial )
npm install @tiptap/react @tiptap/pm @tiptap/starter-kit# Algunas extensiones interesantes
npm install @tiptap/extension-color @tiptap/extension-list-item @tiptap/extension-text-styleimport { useEditor, EditorContent } from '@tiptap/react'
import StarterKit from '@tiptap/starter-kit'
const Tiptap = () => {
const editor = useEditor({
extensions: [
StarterKit,
],
content: '<p>Hello World! 🌎️</p>',
})
return (
<EditorContent editor={editor} />
)
}
export default TiptapOtros editores interesantes
Al usar bibliotecas com Tiptap es muy probable que te encuentres ante la necesidad de rellenar un elemento
dangerouslySetInnerHTML.
Esta propiedad en una aplicación React es equivalente al atributo innerHTML en el DOM del navegador. En JavaScript estándar, innerHTML es un atributo de los elementos DOM que permite obtener o configurar el contenido HTML dentro de un elemento y forma parte de la API DOM estándar, no es específica de React.
Como sustituto de innerHTML, React dispone de dangerouslySetInnerHTML que es una propiedad que se puede usar en elementos HTML de una aplicación React para configurar su contenido mediante programación. En lugar de usar un selector para obtener el elemento HTML y luego configurar su innerHTML, se puede usar esta propiedad directamente en el elemento.
Cuando se usa dangerouslySetInnerHTML, React también sabe que el contenido de ese elemento específico es dinámico y, para los hijos de ese nodo, simplemente omite la comparación con el DOM virtual para obtener un rendimiento adicional.
Imagina que tienes una página web donde se pueden enviar comentarios usando un editor de texto enriquecido. En este caso, el resultado de dicho editor probablemente será HTML con etiquetas como <p>, <b> e <img>.
Cuando se usa dangerouslySetInnerHTML, React toma conciencia de las etiquetas HTML y las procesa correctamente.
const data = 'lorem <b>ipsum</b>';
return (
<div dangerouslySetInnerHTML={{__html: data}} />
// ...
)Hay que tener en cuenta que el valor pasado a dangerouslySetInnerHTML debe ser un objeto con la clave __html. Además, el elemento en el que se utilice la propiedad dangerouslySetInnerHTML no debe tener hijos, de ahí el uso del elemento <div> con etiqueta de cierre automático.
Como sugiere el nombre de la propiedad, usar dangerouslySetInnerHTML puede ser peligroso, ya que expone el código a ataques de scripts entre sitios (XSS). Esto puede convertirse en un problema especialmente grave si se obtienen datos de una fuente externa o se renderiza contenido enviado por los usuarios.
En el muy poco probable caso de que tengamos que incorporar visualización de código fuente en nuestra aplicación, podemos usar la biblioteca bright.
Esta biblioteca debe usarse en el lado servidor, aunque es probable que también pueda emplearse en la lado cliente (no la he utilizado).
npm install brightimport { Code } from "bright"
const myCode = `
let hello = "hello brightness"
console.log(hello, "my old friend")
`.trim()
Code.theme = "github-dark"
export default function Page() {
return (
<Code lang="js" title="example.js" lineNumbers>
{myCode}
</Code>
)
}



























