Tema
Express
Express es el framework web más usado de Node. Simplifica rutas, lectura del body, middleware y respuestas. En esta lección aprenderás sus bases y una estructura de proyecto profesional con rutas, controladores y middleware separados. Usaremos Express 5, la versión actual.
Concepto
Express es una capa delgada sobre http. Su idea central es la cadena de middleware: cada petición pasa por una serie de funciones, como una línea de producción. Cada estación puede revisar, modificar, responder o pasar la petición a la siguiente con next().
txt
Petición → [logger] → [express.json()] → [autenticación] → [ruta] → Respuesta
│
└─ si falla → responde 401Novedades de Express 5 que importan:
- Si una ruta
asynclanza un error o una promesa se rechaza, Express lo manda solo al manejador de errores (ya no necesitastry/catchen cada ruta). - Sintaxis de rutas más estricta: el comodín ahora se escribe
/*rutaen lugar de*. - Requiere Node 18+.
Instalación
bash
mkdir api-express && cd api-express
npm init -y
npm pkg set type=module
npm i expressPrimer servidor
js
// src/index.js
import express from 'express'
const app = express()
const PUERTO = process.env.PORT ?? 3000
// Middleware que convierte el body JSON en req.body
app.use(express.json())
app.get('/', (req, res) => {
res.send('¡Hola desde Express!')
})
app.get('/api/saludo', (req, res) => {
res.json({ mensaje: 'Hola', fecha: new Date() })
})
app.listen(PUERTO, () => {
console.log(`Servidor en http://localhost:${PUERTO}`)
})Compara con la lección anterior: res.json() pone el header, el status y convierte a JSON por ti.
Rutas
js
// Parámetros de ruta: /usuarios/42
app.get('/usuarios/:id', (req, res) => {
res.json({ id: req.params.id }) // ¡siempre llega como string!
})
// Varios parámetros
app.get('/tiendas/:tiendaId/productos/:productoId', (req, res) => {
const { tiendaId, productoId } = req.params
res.json({ tiendaId, productoId })
})
// Query string: /buscar?q=laptop&pagina=2
app.get('/buscar', (req, res) => {
const { q = '', pagina = '1' } = req.query
res.json({ q, pagina: Number(pagina) })
})
// Body (POST/PUT/PATCH) gracias a express.json()
app.post('/contacto', (req, res) => {
const { nombre, email } = req.body
res.status(201).json({ recibido: true, nombre, email })
})
// Métodos de respuesta comunes
app.get('/ejemplos', (req, res) => {
// res.send('texto o html')
// res.json({ ... })
// res.status(404).json({ error: '...' })
// res.sendStatus(204)
// res.redirect('/otra-ruta')
// res.sendFile(rutaAbsoluta)
// res.download(rutaAbsoluta)
res.json({ ok: true })
})Agrupar rutas con Router
js
// src/routes/productos.routes.js
import { Router } from 'express'
const router = Router()
router.get('/', (req, res) => res.json([])) // GET /api/productos
router.get('/:id', (req, res) => res.json({ id: req.params.id }))
router.post('/', (req, res) => res.status(201).json(req.body))
export default routerjs
// en src/index.js
import productosRouter from './routes/productos.routes.js'
app.use('/api/productos', productosRouter)Middleware
Un middleware es una función (req, res, next):
js
// Middleware propio: registra cada petición y cuánto tardó
function logger(req, res, next) {
const inicio = Date.now()
res.on('finish', () => {
console.log(`${req.method} ${req.originalUrl} → ${res.statusCode} (${Date.now() - inicio} ms)`)
})
next() // ¡pasa a la siguiente función!
}
app.use(logger) // global: aplica a todas las rutas
// Middleware sólo para ciertas rutas
function soloAdmin(req, res, next) {
if (req.headers['x-rol'] !== 'admin') {
return res.status(403).json({ error: 'Acceso denegado' })
}
next()
}
app.delete('/api/usuarios/:id', soloAdmin, (req, res) => {
res.sendStatus(204)
})Middleware integrados y populares:
js
app.use(express.json({ limit: '1mb' })) // body JSON
app.use(express.urlencoded({ extended: true })) // formularios HTML
app.use(express.static('public')) // archivos estáticos
// De terceros (los veremos más adelante): cors, helmet, morgan, cookie-parserManejo de errores y 404
js
// 404: va DESPUÉS de todas las rutas
app.use((req, res) => {
res.status(404).json({ error: `No existe ${req.method} ${req.originalUrl}` })
})
// Manejador de errores: 4 parámetros, va al FINAL
app.use((err, req, res, next) => {
console.error(err)
res.status(err.status ?? 500).json({ error: err.message ?? 'Error interno' })
})js
// Con Express 5, un error en una ruta async llega solo al manejador
app.get('/falla', async (req, res) => {
throw new Error('Algo se rompió') // → manejador de errores, status 500
})Estructura de proyecto profesional
Separar responsabilidades hace el código fácil de mantener y probar:
txt
api-express/
├── src/
│ ├── app.js ← configura Express (sin listen)
│ ├── index.js ← arranca el servidor
│ ├── routes/ ← qué URL llama a qué controlador
│ │ └── tareas.routes.js
│ ├── controllers/ ← reciben req, responden res
│ │ └── tareas.controller.js
│ ├── services/ ← lógica de negocio y acceso a datos
│ │ └── tareas.service.js
│ └── middlewares/
│ ├── logger.js
│ └── errores.js
├── package.json
└── .gitignore- Rutas: sólo definen URL + método + middlewares + controlador.
- Controladores: traducen HTTP ↔ lógica (leen
req, llaman al servicio, responden). - Servicios: la lógica pura. No saben nada de
reqnires; así puedes reutilizarlos y probarlos.
js
// Lógica de negocio. Hoy en memoria; mañana una base de datos.
let tareas = [
{ id: 1, titulo: 'Aprender Express', hecha: false }
]
let siguienteId = 2
export function listar({ hecha } = {}) {
if (hecha === undefined) return tareas
return tareas.filter((t) => t.hecha === hecha)
}
export function obtener(id) {
return tareas.find((t) => t.id === id) ?? null
}
export function crear(titulo) {
const tarea = { id: siguienteId++, titulo, hecha: false }
tareas.push(tarea)
return tarea
}
export function actualizar(id, cambios) {
const tarea = obtener(id)
if (!tarea) return null
Object.assign(tarea, cambios)
return tarea
}
export function eliminar(id) {
const antes = tareas.length
tareas = tareas.filter((t) => t.id !== id)
return tareas.length < antes
}js
import * as servicio from '../services/tareas.service.js'
export function listar(req, res) {
const { hecha } = req.query
const filtro = hecha === undefined ? {} : { hecha: hecha === 'true' }
res.json(servicio.listar(filtro))
}
export function obtener(req, res) {
const tarea = servicio.obtener(Number(req.params.id))
if (!tarea) return res.status(404).json({ error: 'Tarea no encontrada' })
res.json(tarea)
}
export function crear(req, res) {
const { titulo } = req.body ?? {}
if (!titulo || typeof titulo !== 'string') {
return res.status(400).json({ error: 'El título es obligatorio' })
}
res.status(201).json(servicio.crear(titulo.trim()))
}
export function actualizar(req, res) {
const { titulo, hecha } = req.body ?? {}
const cambios = {}
if (titulo !== undefined) cambios.titulo = String(titulo)
if (hecha !== undefined) cambios.hecha = Boolean(hecha)
const tarea = servicio.actualizar(Number(req.params.id), cambios)
if (!tarea) return res.status(404).json({ error: 'Tarea no encontrada' })
res.json(tarea)
}
export function eliminar(req, res) {
const ok = servicio.eliminar(Number(req.params.id))
if (!ok) return res.status(404).json({ error: 'Tarea no encontrada' })
res.sendStatus(204)
}js
import { Router } from 'express'
import * as ctrl from '../controllers/tareas.controller.js'
const router = Router()
router.get('/', ctrl.listar)
router.get('/:id', ctrl.obtener)
router.post('/', ctrl.crear)
router.patch('/:id', ctrl.actualizar)
router.delete('/:id', ctrl.eliminar)
export default routerjs
export function logger(req, res, next) {
const inicio = Date.now()
res.on('finish', () => {
console.log(`${req.method} ${req.originalUrl} ${res.statusCode} - ${Date.now() - inicio}ms`)
})
next()
}js
export function noEncontrado(req, res) {
res.status(404).json({ error: `Ruta no encontrada: ${req.method} ${req.originalUrl}` })
}
// eslint-disable-next-line no-unused-vars
export function manejadorErrores(err, req, res, next) {
console.error(err)
const status = err.status ?? 500
res.status(status).json({
error: status === 500 ? 'Error interno del servidor' : err.message
})
}js
import express from 'express'
import { logger } from './middlewares/logger.js'
import { noEncontrado, manejadorErrores } from './middlewares/errores.js'
import tareasRouter from './routes/tareas.routes.js'
const app = express()
app.use(express.json())
app.use(logger)
app.get('/api/salud', (req, res) => res.json({ ok: true }))
app.use('/api/tareas', tareasRouter)
app.use(noEncontrado)
app.use(manejadorErrores)
export default appjs
import app from './app.js'
const PUERTO = process.env.PORT ?? 3000
app.listen(PUERTO, () => console.log(`API en http://localhost:${PUERTO}`))json
{
"name": "api-express",
"type": "module",
"scripts": {
"dev": "node --watch src/index.js",
"start": "node src/index.js"
},
"dependencies": {
"express": "^5.1.0"
}
}bash
npm run dev
curl http://localhost:3000/api/tareas
curl -X POST http://localhost:3000/api/tareas -H "Content-Type: application/json" -d '{"titulo":"Hacer ejercicio"}'
curl -X PATCH http://localhost:3000/api/tareas/1 -H "Content-Type: application/json" -d '{"hecha":true}'Buena práctica
Separa app.js (configuración) de index.js (listen). Así en los tests puedes importar app sin abrir un puerto real. Lo usaremos en la lección de testing.
Error común
El orden importa. Si registras app.use(noEncontrado) antes de tus rutas, todo responderá 404. Y express.json() debe ir antes de las rutas que leen req.body.
Error común
Olvidar return al responder dentro de un if: el código sigue y vuelve a responder, causando Cannot set headers after they are sent to the client.
Ejercicios
- Agrega un middleware
validarIdque responda 400 sireq.params.idno es un número entero positivo, y úsalo en las rutas con:id. - Agrega
GET /api/tareas/estadisticasque devuelva{ total, hechas, pendientes }. Cuidado con el orden respecto a/:id.
Ver solución
js
// src/middlewares/validarId.js
export function validarId(req, res, next) {
const id = Number(req.params.id)
if (!Number.isInteger(id) || id <= 0) {
return res.status(400).json({ error: 'El id debe ser un entero positivo' })
}
next()
}js
// src/routes/tareas.routes.js
import { Router } from 'express'
import * as ctrl from '../controllers/tareas.controller.js'
import { validarId } from '../middlewares/validarId.js'
const router = Router()
router.get('/', ctrl.listar)
// ¡Antes de /:id! Si no, "estadisticas" se tomaría como un id
router.get('/estadisticas', ctrl.estadisticas)
router.get('/:id', validarId, ctrl.obtener)
router.post('/', ctrl.crear)
router.patch('/:id', validarId, ctrl.actualizar)
router.delete('/:id', validarId, ctrl.eliminar)
export default routerjs
// en tareas.controller.js
export function estadisticas(req, res) {
const todas = servicio.listar()
const hechas = todas.filter((t) => t.hecha).length
res.json({ total: todas.length, hechas, pendientes: todas.length - hechas })
}Resumen
- Express organiza el servidor en rutas y middleware
(req, res, next). req.params,req.queryyreq.body(conexpress.json()) dan acceso a los datos.- Express 5 captura errores de funciones
asyncautomáticamente. - Estructura profesional:
routes→controllers→services, conapp.jsseparado deindex.js.