Crea una API Simple con Express.js en 5 Pasos

Express.js sigue siendo la forma más rápida de lanzar una API lista para producción como fundador solitario. Aquí está la cuestión: no necesitas un tutorial de framework que se extienda por páginas. En su lugar, sigue un camino claro desde npm init hasta desplegar endpoints que gestionen autenticación, validación y errores de manera eficiente, incluso bajo carga.

una computadora en un escritorio Foto: Growtika en Unsplash

Para quién es esto

Estás creando un SaaS, un backend de aplicación móvil o una herramienta interna por tu cuenta. Conoces JavaScript pero quieres evitar una inmersión profunda en documentos de framework de 40 páginas. Una API debería estar activa hoy, no en el próximo sprint. Esta guía asume cierta familiaridad con Node.js y un terminal a tu disposición.

Paso 1: Inicializar el Proyecto e Instalar Dependencias Básicas

Comienza creando un nuevo directorio e inicializando npm. Vale la pena señalar: la mayoría de los problemas de despliegue se deben a versiones de dependencias faltantes o desajustadas.

mkdir my-api && cd my-api
npm init -y
npm install express dotenv cors helmet
npm install --save-dev nodemon

Qué hace cada paquete:

  • express: el framework web en sí
  • dotenv: carga variables de entorno desde archivos .env
  • cors: maneja solicitudes de origen cruzado sin dolores de cabeza
  • helmet: establece encabezados de seguridad por defecto
  • nodemon: reinicia el servidor en cambios de archivos durante el desarrollo

Crea un archivo .env en la raíz de tu proyecto:

PORT=3000
NODE_ENV=development

Actualiza los scripts de package.json:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js"
}

Muchos hackers independientes ignoran helmet y envían APIs con encabezados por defecto. Sin embargo, la documentación de mejores prácticas de seguridad de Express.js destaca que la falta de encabezados de seguridad puede llevar a vulnerabilidades graves en APIs de producción.

Paso 2: Configurar un Servidor Básico con Middleware

Pantalla de computadora mostrando código con un menú contextual Foto: Daniil Komov en Unsplash

Crea server.js en tu directorio raíz. Aquí es donde reside tu API.

require('dotenv').config();
const express = require('express');
const helmet = require('helmet');
const cors = require('cors');

const app = express();
const PORT = process.env.PORT || 3000;

// Middleware
app.use(helmet());
app.use(cors());
app.use(express.json());
app.use(express.urlencoded({ extended: true }));

// Endpoint de verificación de salud
app.get('/health', (req, res) => {
  res.status(200).json({ status: 'ok', timestamp: new Date().toISOString() });
});

// Manejador 404
app.use((req, res) => {
  res.status(404).json({ error: 'Ruta no encontrada' });
});

// Manejador de errores
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ 
    error: 'Error interno del servidor',
    message: process.env.NODE_ENV === 'development' ? err.message : undefined
  });
});

app.listen(PORT, () => {
  console.log(`Servidor corriendo en el puerto ${PORT}`);
});

El orden del middleware importa. Comienza con middleware de seguridad como helmet(). Los analizadores de cuerpo como express.json() siguen a continuación. Luego, los manejadores de rutas. Finalmente, los manejadores de errores van al final.

El endpoint de verificación de salud no es opcional. Las plataformas de despliegue lo esperan para monitoreo, chequeos de balanceadores de carga y depuración de problemas en producción.

Ejecuta tu servidor:

npm run dev

Prueba:

curl http://localhost:3000/health

Espera {"status":"ok","timestamp":"2026-01-15T10:30:00.000Z"} o similar.

Paso 3: Construir Rutas CRUD con Estructura Adecuada

Muchos fundadores solitarios agrupan todas las rutas en un solo archivo. Este enfoque está bien hasta que llegues a la ruta 15. Separa las preocupaciones desde el principio.

Crea un directorio routes/ y añade routes/users.js:

const express = require('express');
const router = express.Router();

// Almacenamiento en memoria (reemplazar con base de datos en producción)
let users = [
  { id: 1, name: 'Alice', email: 'alice@example.com' },
  { id: 2, name: 'Bob', email: 'bob@example.com' }
];

// GET todos los usuarios
router.get('/', (req, res) => {
  res.json(users);
});

// GET usuario único
router.get('/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
  res.json(user);
});

// POST nuevo usuario
router.post('/', (req, res) => {
  const { name, email } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ error: 'Se requieren nombre y correo electrónico' });
  }
  
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  
  users.push(newUser);
  res.status(201).json(newUser);
});

// PUT actualizar usuario
router.put('/:id', (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
  
  const { name, email } = req.body;
  if (name) user.name = name;
  if (email) user.email = email;
  
  res.json(user);
});

// DELETE usuario
router.delete('/:id', (req, res) => {
  const index = users.findIndex(u => u.id === parseInt(req.params.id));
  if (index === -1) return res.status(404).json({ error: 'Usuario no encontrado' });
  
  users.splice(index, 1);
  res.status(204).send();
});

module.exports = router;

Actualiza server.js para montar las rutas:

const userRoutes = require('./routes/users');

// Añadir después del middleware, antes de los manejadores de errores
app.use('/api/users', userRoutes);

Prueba tus operaciones CRUD:

# Obtener todos los usuarios
curl http://localhost:3000/api/users

# Crear usuario
curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"Charlie","email":"charlie@example.com"}'

# Obtener usuario único
curl http://localhost:3000/api/users/1

# Actualizar usuario
curl -X PUT http://localhost:3000/api/users/1 \
  -H "Content-Type: application/json" \
  -d '{"name":"Alice Updated"}'

# Eliminar usuario
curl -X DELETE http://localhost:3000/api/users/1

Este almacenamiento en memoria está bien para prototipos. Para producción, cámbialo por PostgreSQL a través de node-postgres, MongoDB a través de Mongoose, o SQLite a través de better-sqlite3.

Paso 4: Añadir Validación de Entrada y Manejo de Errores

Capturar errores con validación de req.body en crudo puede ser tardío. Instala un validador:

npm install joi

Crea middleware/validate.js:

const Joi = require('joi');

const schemas = {
  user: Joi.object({
    name: Joi.string().min(2).max(50).required(),
    email: Joi.string().email().required()
  }),
  userUpdate: Joi.object({
    name: Joi.string().min(2).max(50),
    email: Joi.string().email()
  }).min(1) // Se requiere al menos un campo
};

const validate = (schema) => {
  return (req, res, next) => {
    const { error } = schemas[schema].validate(req.body);
    if (error) {
      return res.status(400).json({ 
        error: 'La validación falló', 
        details: error.details.map(d => d.message)
      });
    }
    next();
  };
};

module.exports = validate;

Actualiza routes/users.js para usar la validación:

const validate = require('../middleware/validate');

// Reemplazar ruta POST
router.post('/', validate('user'), (req, res) => {
  const { name, email } = req.body;
  
  const newUser = {
    id: users.length + 1,
    name,
    email
  };
  
  users.push(newUser);
  res.status(201).json(newUser);
});

// Reemplazar ruta PUT
router.put('/:id', validate('userUpdate'), (req, res) => {
  const user = users.find(u => u.id === parseInt(req.params.id));
  if (!user) return res.status(404).json({ error: 'Usuario no encontrado' });
  
  const { name, email } = req.body;
  if (name) user.name = name;
  if (email) user.email = email;
  
  res.json(user);
});

Prueba una entrada inválida:

curl -X POST http://localhost:3000/api/users \
  -H "Content-Type: application/json" \
  -d '{"name":"A","email":"not-an-email"}'

Recibirás un error de validación claro en lugar de un fallo 500.

Paso 5: Desplegar en Producción con Configuración de Entorno

La mayoría de los fundadores solitarios despliegan erróneamente con NODE_ENV=development. Configura una separación adecuada de entornos.

Actualiza .env para el desarrollo local. Crea .env.production (NO lo comitas):

PORT=8080
NODE_ENV=production
DATABASE_URL=your_database_connection_string
API_KEY=your_production_api_key

Añade a .gitignore:

node_modules/
.env
.env.production
.env.local

Para el despliegue, considera Railway, Render o Fly.io. Los tres soportan despliegues de Express.js sin necesidad de configuración de Docker.

Ejemplo de Railway:

  1. Instala Railway CLI: npm i -g @railway/cli
  2. Inicia sesión: railway login
  3. Inicializa: railway init
  4. Configura variables de entorno en el panel de Railway
  5. Despliega: railway up

Ejemplo de Render:

  1. Conecta el repositorio de GitHub en el panel de Render
  2. Selecciona "Servicio Web"
  3. Comando de construcción: npm install
  4. Comando de inicio: npm start
  5. Añade variables de entorno en el panel
  6. Despliega

Según la guía de mejores prácticas de producción de Node.js, siempre ejecuta con NODE_ENV=production para habilitar la caché de plantillas, reducir el registro detallado y aumentar significativamente el rendimiento.

Configura un simple script de verificación de despliegue en package.json:

"scripts": {
  "start": "node server.js",
  "dev": "nodemon server.js",
  "check": "node -e \"console.log('Versión de Node:', process.version); console.log('Env:', process.env.NODE_ENV)\""
}

Ejecuta después del despliegue:

npm run check

Lo Que Nadie Te Dice Sobre las APIs de Express

El manejo de errores asíncronos se rompe silenciosamente. Envuelve los manejadores de rutas asíncronas o usa express-async-errors:

npm install express-async-errors

Añade al principio de server.js:

require('express-async-errors');

Ahora los errores asíncronos son capturados automáticamente por tu manejador de errores.

La limitación de tasa no es opcional. Tu API de nivel gratuito será golpeada. Instala protección:

npm install express-rate-limit

Añade a server.js:

const rateLimit = require('express-rate-limit');

const limiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100 // limita cada IP a 100 solicitudes por windowMs
});

app.use('/api/', limiter);

La mala configuración de CORS arruina lanzamientos. Si tu frontend tiene problemas para alcanzar tu API, verifica CORS primero. Para desarrollo con orígenes específicos:

app.use(cors({
  origin: process.env.NODE_ENV === 'production' 
    ? 'https://yourdomain.com' 
    : 'http://localhost:5173'
}));

El registro es importante. Console.log desaparece en producción. Usa un registrador simple:

npm install pino pino-pretty

Reemplaza console.log con registro estructurado, pero eso es un artículo separado.

Errores Comunes que Cometen los Fundadores Solitarios

Omitir la validación de entrada hasta la producción. Las APIs pueden lanzar errores 500 cuando alguien envía "age": "twenty" en lugar de "age": 20. Valida a nivel de ruta, siempre.

No manejar solicitudes OPTIONS para el preflight de CORS. Express con CORS maneja esto, pero middleware personalizado podría romperlo. Prueba con un frontend real, no solo con curl.

Ejecutar operaciones sincrónicas en los manejadores de rutas. Leer archivos con fs.readFileSync() o hacer trabajo intensivo en CPU bloquea todo el bucle de eventos. Usa alternativas asíncronas o descarga a una cola de trabajo.

Desplegar sin chequeos de salud. Las plataformas de despliegue requieren que /health devuelva 200. Añádelo primero, no cuando surjan problemas.

Olvidar establecer trust proxy detrás de un proxy inverso (por ejemplo, Nginx, balanceadores de carga). Sin esto, req.ip devuelve la IP del proxy, no la del cliente:

app.set('trust proxy', 1);

FAQ

¿Necesito un framework como NestJS o Fastify en lugar de Express?

No. Express maneja el 90% de los casos de uso de fundadores solitarios. NestJS añade decoradores de TypeScript e inyección de dependencias—ideal para equipos, pero excesivo para proyectos en solitario. Fastify es más rápido a gran escala, pero no notarás la diferencia hasta superar las 10,000 solicitudes por segundo. Comienza con Express, migra solo si aparecen cuellos de botella claros.

¿Cómo añado autenticación a estas rutas?

Instala jsonwebtoken y bcrypt. Crea una ruta /auth/login que devuelva un JWT. Añade middleware para verificar el token en rutas protegidas. El patrón: el middleware verifica Authorization: Bearer <token>, lo verifica con jwt.verify(), adjunta el usuario a req.user y llama a next().

¿Debería usar TypeScript con Express?

Si TypeScript es parte de la rutina diaria, sí. Si no, JavaScript permite un envío más rápido para proyectos en solitario. TypeScript introduce configuración adicional de compilación y requiere definiciones de tipo para cada paquete, lo que puede ralentizar la iteración. La mayoría de los errores de API en solitario provienen de la falta de validación, no de errores de tipo. Usa Joi para la validación en tiempo de ejecución—captura lo que TypeScript no puede.

¿Cuál es la forma más rápida de añadir una base de datos?

Para prototipos: SQLite con better-sqlite3. Para producción: PostgreSQL a través de Supabase o el Postgres integrado de Railway. Omite los ORM inicialmente—escribe SQL en bruto o usa un generador de consultas como knex. Sequelize y TypeORM añaden complejidad innecesaria para 0-1000 usuarios.

Conclusión

Ahora tienes una estructura de API de Express.js lista para producción que maneja eficientemente rutas, validación, errores y seguridad. No añadas más características—despliega esto, conecta tu frontend y preséntalo a usuarios reales.

Próximo paso: Sube tu código a GitHub, enlázalo a Railway o Render, y despliega en los próximos 15 minutos. La verdadera prueba para encontrar casos límite faltantes es ejecutar tu API bajo tráfico real. Para prototipos rápidos, considera herramientas como Bubble vs. Webflow: Which Is Best for Rapid Prototyping? para agilizar tu proceso de desarrollo.


Nota editorial: Este artículo fue elaborado con asistencia de IA y revisado por Javier Valencia. A lo largo del texto se distinguen los hechos verificados de la opinión editorial. Las fuentes externas enlazadas son independientes de NewsTide.

Nota editorial: Este artículo ha sido elaborado con asistencia de inteligencia artificial y revisado por Javier Valencia para garantizar su precisión y relevancia. Conoce nuestra política editorial.

Más sobre Dev Stack

← Volver al inicioVer todos de Dev Stack