¿Qué es Express?
Express es un framework web minimalista para Node.js. No impone una estructura de carpetas, una base de datos ni una arquitectura específica. Simplemente te ofrece tres cosas y te deja trabajar: una forma de hacer match con URLs, un objeto de solicitud (request) y otro de respuesta (response), y una cadena de middleware que los conecta.
Esa simplicidad es precisamente su objetivo. Express llegó en 2010, cuando el módulo http integrado de Node requería que escribieras el enrutamiento y el parseo del cuerpo de las peticiones a mano. Express redujo esas tareas a unas pocas líneas. Quince años después, sigue siendo la opción predeterminada para APIs en Node, y sus conceptos han sido adoptados por casi todos los frameworks posteriores.
Si entiendes Express, entiendes la estructura de JavaScript en el lado del servidor.
El ciclo de solicitud y respuesta
En esencia, una aplicación de Express es una función que recibe una solicitud y envía una respuesta. Todo lo demás es una capa de conveniencia.
import express from "express";
const app = express();
app.get("/", (req, res) => {
res.send("Hello, world");
});
El objeto req envuelve el mensaje entrante de Node: req.params, req.query, req.body y req.headers te proporcionan la entrada. El objeto res envuelve el mensaje saliente: res.status(), res.json(), res.send() y res.set() dan forma a la salida. Un handler finaliza la solicitud llamando a uno de los métodos res.
Enrutamiento
Una ruta es un método, una ruta (path) y uno o más manejadores (handlers). Las rutas pueden ser estáticas, parametrizadas con :name, o coincidir con patrones.
app.get("/posts", listPosts);
app.get("/posts/:id", getPost);
app.post("/posts", createPost);
app.put("/posts/:id", replacePost);
app.patch("/posts/:id", updatePost);
app.delete("/posts/:id", deletePost);
Los parámetros de la ruta llegan en req.params, y las query strings en req.query.
app.get("/posts/:id", (req, res) => {
const { id } = req.params; // "/posts/42" -> "42"
const { fields } = req.query; // "?fields=title" -> "title"
res.json({ id, fields });
});
También puedes pasar varios manejadores a una sola ruta. Estos se ejecutan en orden hasta que uno finaliza la respuesta, que es la forma en que adjuntas middleware por ruta, como la validación o la autorización.
app.post("/posts", requireAuth, validatePost, createPost);
Middleware: el concepto fundamental que debes dominar
El middleware es el corazón de Express. Es cualquier función que recibe (req, res, next) y, o bien finaliza la respuesta, o llama a next() para continuar.
function logger(req, res, next) {
const start = Date.now();
res.on("finish", () => {
console.log(`${req.method} ${req.url} ${res.statusCode} ${Date.now() - start}ms`);
});
next();
}
app.use(logger);
El middleware se ejecuta en el orden en que se registra. Esa única regla explica la mayor parte del comportamiento de Express: registra el parser de JSON antes de las rutas que lean req.body, registra la autenticación antes de las rutas protegidas y registra el manejador de errores 404 después de todas las rutas.
Hay tres tipos que vale la pena conocer:
- Application middleware —
app.use(fn)se ejecuta en cada solicitud. - Router middleware —
router.use(fn)se ejecuta solo para las rutas de ese router. - Route middleware —
app.get(path, fn, handler)se ejecuta solo para esa ruta.
Un middleware que recibe cuatro argumentos es un error handler y solo se ejecuta cuando algo llama a next(err).
Los routers mantienen la legibilidad en aplicaciones grandes
A medida que una aplicación crece, un único archivo de rutas se vuelve inmanejable. express.Router() te permite agrupar rutas relacionadas y montarlas bajo un prefijo.
// routes/posts.js
import { Router } from "express";
const router = Router();
router.get("/", listPosts);
router.post("/", createPost);
export default router;
// app.js
import posts from "./routes/posts.js";
app.use("/api/v1/posts", posts);
Ahora, cada archivo es dueño de un recurso y el prefijo de la URL reside en un solo lugar. Esta es la estructura que evita que los proyectos de Express se conviertan en un montón desordenado de endpoints.
Manejo de errores
Los errores deben fluir hacia un único lugar. Llama a next(err) desde cualquier parte y Express saltará directamente al primer manejador de errores de cuatro argumentos.
app.get("/users/:id", async (req, res, next) => {
try {
const user = await db.user.findById(req.params.id);
if (!user) return res.status(404).json({ error: "not_found" });
res.json(user);
} catch (err) {
next(err);
}
});
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status ?? 500).json({
error: err.code ?? "internal_error",
message: err.message,
});
});
Express 5 reenvía las promesas rechazadas al manejador de errores automáticamente, por lo que los manejadores async ya no necesitan un wrapper. Aun así, ser explícito con try/catch hace que la intención sea obvia y mantiene el código portable. Registra un manejador 404 después de todas las rutas para que las rutas no coincidentes devuelvan un error JSON adecuado en lugar de la página HTML predeterminada de Express.
Lectura del cuerpo de la solicitud
Express no analiza los cuerpos de las solicitudes por defecto. Añade los parsers integrados antes de las rutas que los necesiten.
app.use(express.json()); // application/json
app.use(express.urlencoded({ extended: true })); // form posts
Para la subida de archivos, utiliza multer. Para una validación más robusta, combina el parsing con una librería de esquemas como Zod o Joi y rechaza las entradas incorrectas lo antes posible, antes de que lleguen a tus handlers.
Conceptos básicos de seguridad y producción
Express es deliberadamente minimalista, por lo que unas pocas líneas de middleware cubren lo esencial:
import helmet from "helmet";
import cors from "cors";
import rateLimit from "express-rate-limit";
app.use(helmet()); // sensible security headers
app.use(cors({ origin: "https://app.example.com" }));
app.use(rateLimit({ windowMs: 60_000, max: 100 }));
Además, desactiva x-powered-by, mantén las dependencias actualizadas, valida y limita el tamaño de las solicitudes, y nunca confíes en la entrada del cliente. Estas son las mismas preocupaciones que tiene cualquier backend; Express simplemente te ofrece un lugar pequeño y conocido para gestionar cada una de ellas.
Pruebas
Dado que los handlers son funciones simples, Express es fácil de testear. supertest arranca la aplicación en el mismo proceso y realiza aserciones HTTP reales sin necesidad de abrir un puerto.
import request from "supertest";
import app from "../app.js";
test("GET /posts returns a list", async () => {
const res = await request(app).get("/posts").expect(200);
expect(Array.isArray(res.body)).toBe(true);
});
Separa la definición de la aplicación (app.js) del inicio del servidor (server.js) para que los tests puedan importar la app sin ponerse a escuchar peticiones. Esa simple división hace que las pruebas de integración sean triviales.
Mejores prácticas
- Mantén los route handlers ligeros; mueve la lógica a servicios y capas de acceso a datos.
- Divide las rutas por funcionalidad con
express.Router()y móntalas bajo un prefijo de versión. - Registra el middleware de forma deliberada y en orden: primero los parsers, luego la autenticación, luego las rutas, luego el 404 y finalmente los errores.
- Redirige cada error con
next(err)y formatea las respuestas en un solo lugar. - Valida y limita todas las entradas antes de que lleguen a la lógica de negocio.
- Utiliza
helmet,corsy rate limiting desde el primer día. - Separa la aplicación del servidor para que los tests sigan siendo rápidos.
Errores comunes
- Olvidar
express.json()y preguntarse por quéreq.bodyes undefined. - Ignorar promesas rechazadas en handlers asíncronos en Express 4.
- Registrar el handler de 404 o de errores antes de las rutas que debería capturar.
- Dejar que un único archivo de rutas crezca hasta tener cientos de líneas.
- Devolver un 200 para errores en lugar de utilizar códigos de estado.
- Confiar en
req.bodysin realizar una validación. - Mezclar
res.send,res.jsonyres.endde forma impredecible.
Próximos pasos
Express te enseña el request, el response y la cadena de middleware — conceptos que reutilizarás siempre. Si buscas más velocidad y validación integrada, lee a continuación la guía de Fastify. Si prefieres estructura e inyección de dependencias para un equipo grande, pasa a NestJS. Para despliegues en edge y serverless, Hono ofrece el mismo estilo sobre un runtime basado en estándares web. Y si sientes que todo esto fue muy rápido, repasa los conceptos básicos de Node.js que sustentan todo esto.