Tema
Servidor Express base
Un punto de partida profesional para cualquier API: variables de entorno validadas, separación entre app y server, rutas modulares, healthcheck, logs y cierre ordenado. Todos los demás snippets de Node se montan sobre esta estructura.
Instalación
bash
npm init -y
npm pkg set type=module
npm install express dotenv morgan
npm install -D nodemonjson
{
"name": "mi-api",
"version": "1.0.0",
"type": "module",
"scripts": {
"dev": "nodemon src/server.js",
"start": "node src/server.js"
},
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.21.0",
"morgan": "^1.10.0"
},
"devDependencies": {
"nodemon": "^3.1.4"
}
}¿Sin nodemon?
Desde Node 18.11 puedes usar node --watch src/server.js, que reinicia solo al guardar.
Archivos
env
NODE_ENV=development
PORT=3000
APP_URL=http://localhost:3000js
// Carga .env y valida que existan las variables necesarias
import 'dotenv/config';
const requeridas = ['PORT'];
for (const nombre of requeridas) {
if (!process.env[nombre]) {
console.error(`Falta la variable de entorno ${nombre}`);
process.exit(1);
}
}
export const env = {
nodeEnv: process.env.NODE_ENV ?? 'development',
esProduccion: process.env.NODE_ENV === 'production',
port: Number(process.env.PORT),
appUrl: process.env.APP_URL ?? `http://localhost:${process.env.PORT}`,
};js
import express from 'express';
import morgan from 'morgan';
import { env } from './config/env.js';
import rutas from './routes/index.js';
const app = express();
// Si estás detrás de Nginx, Render, Railway, etc. (para obtener la IP real)
app.set('trust proxy', 1);
// Middlewares globales
app.use(express.json({ limit: '1mb' })); // cuerpo JSON
app.use(express.urlencoded({ extended: true })); // formularios
app.use(morgan(env.esProduccion ? 'combined' : 'dev')); // logs de peticiones
app.use(express.static('public')); // archivos estáticos opcionales
// Healthcheck para monitoreo y balanceadores
app.get('/health', (req, res) => res.json({ ok: true, uptime: process.uptime() }));
// Rutas de la API
app.use('/api', rutas);
// 404 para rutas no definidas
app.use((req, res) => {
res.status(404).json({ error: 'Ruta no encontrada' });
});
// Manejador de errores (4 parámetros). Ver snippet "Middleware de errores" para la versión completa
app.use((err, req, res, next) => {
console.error(err);
res.status(err.status ?? 500).json({ error: env.esProduccion ? 'Error interno' : err.message });
});
export default app;js
import app from './app.js';
import { env } from './config/env.js';
const server = app.listen(env.port, () => {
console.log(`🚀 API lista en ${env.appUrl} (${env.nodeEnv})`);
});
// Cierre ordenado: termina las peticiones en curso antes de salir
function apagar(senal) {
console.log(`${senal} recibido, cerrando servidor...`);
server.close(() => {
console.log('Servidor cerrado');
process.exit(0);
});
setTimeout(() => process.exit(1), 10_000).unref(); // por si algo se queda colgado
}
process.on('SIGTERM', () => apagar('SIGTERM'));
process.on('SIGINT', () => apagar('SIGINT'));
// Errores que nadie capturó: registra y sal (PM2/Docker reinicia el proceso)
process.on('unhandledRejection', (err) => {
console.error('Promesa rechazada sin manejar:', err);
process.exit(1);
});js
import { Router } from 'express';
import productosRouter from './productos.routes.js';
const router = Router();
router.get('/', (req, res) => res.json({ api: 'Mi API', version: '1.0.0' }));
router.use('/productos', productosRouter);
export default router;js
import { Router } from 'express';
import * as ctrl from '../controllers/productos.controller.js';
const router = Router();
router.get('/', ctrl.listar);
router.get('/:id', ctrl.obtener);
router.post('/', ctrl.crear);
export default router;js
// Datos en memoria para probar la estructura.
// En el snippet "CRUD con MySQL" se sustituyen por consultas reales.
const productos = [
{ id: 1, nombre: 'Café de Chiapas 500 g', precio: 189 },
{ id: 2, nombre: 'Taza de barro', precio: 95 },
];
export function listar(req, res) {
res.json({ data: productos });
}
export function obtener(req, res) {
const producto = productos.find((p) => p.id === Number(req.params.id));
if (!producto) return res.status(404).json({ error: 'Producto no encontrado' });
res.json({ data: producto });
}
export function crear(req, res) {
const { nombre, precio } = req.body;
if (!nombre || typeof precio !== 'number') {
return res.status(422).json({ error: 'nombre y precio son obligatorios' });
}
const nuevo = { id: productos.length + 1, nombre, precio };
productos.push(nuevo);
res.status(201).json({ data: nuevo });
}Ejecutar y probar
bash
npm run dev
curl http://localhost:3000/health
curl http://localhost:3000/api/productos
curl -X POST http://localhost:3000/api/productos -H "Content-Type: application/json" -d '{"nombre":"Molino","precio":450}'.gitignore
txt
node_modules/
.env
uploads/
*.log¿Por qué separar app.js y server.js?
app.js sólo configura Express y lo exporta; server.js lo pone a escuchar. Así puedes importar app en tus pruebas (con Supertest) sin abrir un puerto real.
Error común
Olvidar "type": "module" en package.json y obtener "Cannot use import statement outside a module". Y con ESM, las rutas de import llevan extensión: ./app.js, no ./app.
Siguientes pasos
- Conectar una base de datos: MySQL o PostgreSQL.
- Añadir errores centralizados y seguridad.