Servidores web con Express
En esta lección
Hasta ahora todo tu código se ejecutaba en el navegador. Express te permite escribir la otra mitad de una aplicación web, el servidor, con el mismo lenguaje: JavaScript.
Express es un framework para Node.js que facilita crear servidores web y APIs. Es libre, gratuito y uno de los más usados del mundo. Cumple el mismo papel que Django en Python, Laravel en PHP o Spring en Java.
Instalar
En una carpeta nueva para tu proyecto:
npm init -y
npm install express
Y en el package.json, añade "type": "module" para poder usar import:
{
"name": "mi-servidor",
"type": "module",
"dependencies": {
"express": "^5.1.0"
}
}
El servidor mínimo
Crea servidor.js:
import express from "express";
const app = express();
const puerto = process.env.PORT || 3000;
app.get("/", (req, res) => {
res.send("¡Hola desde Express!");
});
app.listen(puerto, () => {
console.log(`Servidor en http://localhost:${puerto} (Ctrl + C para parar)`);
});
Ejecútalo con node servidor.js y abre http://localhost:3000 en el navegador.
express()crea la aplicación.app.get(ruta, manejador)dice qué hacer cuando llega una petición GET a esa ruta.- El manejador recibe dos objetos:
req(request, la petición: qué ha pedido el cliente) yres(response, la respuesta que vas a enviar). app.listen(puerto, ...)arranca el servidor.process.env.PORTlee el puerto de una variable de entorno (lo que usan los servicios de hosting); si no existe, usa el 3000.
Cada vez que cambies el código tienes que parar el servidor (Ctrl + C) y volver a arrancarlo. Para que se reinicie solo: node --watch servidor.js.
Si ves un error como EADDRINUSE, el puerto ya está ocupado: seguramente tienes otro servidor abierto en otra terminal.
Rutas
Cada combinación de método HTTP y ruta tiene su manejador:
app.get("/about", (req, res) => {
res.type("text/plain; charset=utf-8");
res.send("Esto es una prueba de Express");
});
app.get("/api/cursos", (req, res) => {
res.json([
{ id: "html", nombre: "HTML" },
{ id: "css", nombre: "CSS" },
]);
});
res.send(texto)responde con texto o HTML. Conres.type(...)indicas de qué tipo es y su codificación.res.json(objeto)responde con JSON: convierte el objeto conJSON.stringifyy pone la cabecera adecuada. No lo conviertas tú antes, o enviarás un texto entrecomillado dentro de otro.
Parámetros en la ruta
Los segmentos que empiezan por : capturan lo que haya en esa posición de la URL:
app.get("/api/cursos/:id", (req, res) => {
const id = req.params.id;
res.json({ mensaje: `Me has pedido el curso ${id}` });
});
app.get("/api/coordenadas/:x/:y", (req, res) => {
const { x, y } = req.params;
res.json({ x: Number(x), y: Number(y) });
});
Una petición a /api/coordenadas/150/300 da x = "150" e y = "300". Los parámetros siempre llegan como texto.
Parámetros de consulta (query)
Lo que va después de ? en la URL está en req.query:
// GET /api/buscar?q=flexbox&pagina=2
app.get("/api/buscar", (req, res) => {
const texto = req.query.q; // "flexbox"
const pagina = Number(req.query.pagina) || 1;
res.json({ texto, pagina });
});
Regla general: los parámetros de ruta identifican un recurso (/cursos/html); los de consulta filtran, ordenan o paginan (?pagina=2&orden=nombre).
Recibir datos: POST y PUT
Para leer el JSON que envía el cliente en el cuerpo de la petición, activa el middleware express.json():
app.use(express.json());
app.post("/api/tareas", (req, res) => {
const tarea = req.body; // el objeto enviado por el cliente
console.log("Nueva tarea:", tarea);
res.status(201).json({ id: 7, ...tarea });
});
Un middleware es una función que se ejecuta con todas las peticiones antes de llegar a las rutas. express.json() lee el cuerpo y lo convierte en objeto. (En tutoriales antiguos verás la librería body-parser para esto; desde 2017 no hace falta.)
Las peticiones GET puedes probarlas en el navegador, pero para POST, PUT y DELETE necesitas una herramienta: Postman, Bruno, la extensión Thunder Client de VS Code, o la terminal con curl:
curl -X POST http://localhost:3000/api/tareas \
-H "Content-Type: application/json" \
-d '{"titulo": "Estudiar Express"}'
Servir archivos estáticos
Un servidor también debe poder enviar archivos tal cual: tu HTML, CSS, JavaScript e imágenes. Con express.static sirves una carpeta entera:
import path from "node:path";
const carpetaPublica = path.join(import.meta.dirname, "public");
app.use(express.static(carpetaPublica));
Si en public/ tienes index.html, css/estilos.css y js/app.js, el navegador podrá pedirlos como http://localhost:3000/, /css/estilos.css y /js/app.js.
Fíjate: el nombre de la carpeta (public) no aparece en la URL. Esa carpeta es la raíz del sitio web, lo que el navegador ve como /. No la confundas con la raíz del disco duro del servidor, que nunca debe ser accesible desde la web.
Dos buenas prácticas con las rutas en el servidor:
- Construye las rutas con
path.join, que pone las barras correctas en cualquier sistema operativo. - Calcula la carpeta a partir de dónde está tu archivo (
import.meta.dirname, disponible desde Node.js 20.11) o de una variable de entorno, nunca escribas tu ruta personal (/home/ana/proyecto/public): en otro ordenador no existe. (Verás muchos tutoriales conexpress.static("public"): funciona, pero depende de desde qué carpeta arranques el servidor, y confunde bastante.)
Errores: 404 y 500
Después de todas las rutas, añade los manejadores de error. El orden importa: Express prueba las rutas de arriba abajo, y estos solo deben ejecutarse si ninguna ha respondido.
// 404: ninguna ruta coincide
app.use((req, res) => {
res.status(404).type("text/plain").send("404 - No encontrado");
});
// 500: algo ha lanzado un error (tiene 4 parámetros)
app.use((err, req, res, next) => {
console.error(err);
res.status(500).type("text/plain").send("500 - Error en el servidor");
});
Códigos de estado
Cada respuesta lleva un código de estado que le dice al cliente cómo ha ido. Los más habituales:
| Código | Significado | Cuándo usarlo |
|---|---|---|
200 OK | Todo bien | Respuesta normal (es el valor por defecto) |
201 Created | Creado | Tras crear algo con POST |
204 No Content | Bien, sin contenido | Tras borrar algo |
400 Bad Request | Petición incorrecta | Faltan datos o tienen mal formato |
401 Unauthorized | No identificado | Hay que iniciar sesión |
403 Forbidden | Prohibido | Identificado, pero sin permiso |
404 Not Found | No existe | El recurso pedido no existe |
500 Internal Server Error | Error del servidor | Un fallo inesperado en tu código |
- Los 4xx son “culpa” del cliente; para el servidor no son un error, sino una situación normal que hay que gestionar.
- Los 5xx son fallos del servidor: hay que investigarlos.
- Una lista vacía no es un error: si alguien pide los pedidos de un cliente que no tiene ninguno, la respuesta correcta es
200con[], no un 404.
El programa completo
import express from "express";
import path from "node:path";
const app = express();
const puerto = process.env.PORT || 3000;
app.use(express.json());
app.use(express.static(path.join(import.meta.dirname, "public")));
app.get("/api/saludo/:nombre", (req, res) => {
res.json({ mensaje: `Hola, ${req.params.nombre}` });
});
app.post("/api/eco", (req, res) => {
res.json({ recibido: req.body });
});
app.use((req, res) => res.status(404).send("404 - No encontrado"));
app.use((err, req, res, next) => {
console.error(err);
res.status(500).send("500 - Error en el servidor");
});
app.listen(puerto, () => console.log(`Servidor en http://localhost:${puerto}`));
En la siguiente lección usarás todo esto para crear una API REST completa y conectarla con una página.
Pon a prueba lo que has aprendido
¿Te ha quedado claro? Márcala y verás tu progreso en el explorador.