Skip to content

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 nodemon
json
{
  "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:3000
js
// 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 ​

Academia Web — aprende, copia y construye.