Cómo Crear una API REST con Node.js y Express: Tutorial Paso a Paso
Programación

Cómo Crear una API REST con Node.js y Express: Tutorial Paso a Paso

9 min de lectura
26 Vistas
Compartir:

Construir una API REST es una de las habilidades más demandadas en desarrollo web. En este tutorial vamos a crear una API completa desde cero con Node.js y Express — incluyendo CRUD, validación, manejo de errores, middleware de autenticación y estructura profesional de proyecto. No solo el código funcional, sino la arquitectura que uso en proyectos reales.

¿Qué vamos a construir?

Una API REST para gestionar una lista de tareas (todo-list) con las siguientes funcionalidades:

  • CRUD completo (Crear, Leer, Actualizar, Eliminar)
  • Validación de datos de entrada
  • Manejo centralizado de errores
  • Middleware de autenticación con API Key
  • Estructura de carpetas profesional
  • Variables de entorno con dotenv

El código fuente completo está disponible para que lo descargues y lo ejecutes. Vamos paso a paso.

Requisitos previos

Necesitas tener instalado:

  • Node.js versión 18 o superior (recomiendo la versión LTS)
  • Un editor de código — VS Code es ideal con la extensión REST Client o Thunder Client
  • Terminal / línea de comandos

Verifica tu instalación:

node --version   # v20.x.x o superior
npm --version    # 10.x.x o superior

Paso 1: Inicializar el proyecto

# Crear directorio del proyecto
mkdir todo-api && cd todo-api

# Inicializar package.json
npm init -y

# Instalar dependencias
npm install express dotenv uuid

# Instalar dependencias de desarrollo
npm install -D nodemon

Actualiza el package.json para agregar scripts útiles:

{
  "name": "todo-api",
  "version": "1.0.0",
  "scripts": {
    "start": "node src/index.js",
    "dev": "nodemon src/index.js"
  }
}

Paso 2: Estructura de carpetas

Esta es la estructura que uso en mis proyectos Node.js. Separar responsabilidades desde el inicio te ahorra dolores de cabeza cuando el proyecto crece:

todo-api/
├── src/
│   ├── index.js          # Punto de entrada
│   ├── app.js            # Configuración de Express
│   ├── routes/
│   │   └── tareas.js     # Rutas de tareas
│   ├── controllers/
│   │   └── tareasController.js  # Lógica de negocio
│   ├── middleware/
│   │   ├── auth.js       # Autenticación
│   │   ├── errorHandler.js  # Manejo de errores
│   │   └── validator.js  # Validación
│   └── data/
│       └── store.js      # Almacenamiento en memoria
├── .env
├── .gitignore
└── package.json

Paso 3: Configuración base de Express

Crea el archivo .env en la raíz del proyecto:

PORT=3000
API_KEY=mi-clave-secreta-2026
NODE_ENV=development

El archivo src/app.js — configuración central de Express:

const express = require('express');
const tareasRoutes = require('./routes/tareas');
const errorHandler = require('./middleware/errorHandler');

const app = express();

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

// Ruta de health check
app.get('/api/health', (req, res) => {
  res.json({
    status: 'ok',
    timestamp: new Date().toISOString(),
    uptime: process.uptime(),
  });
});

// Rutas de la API
app.use('/api/tareas', tareasRoutes);

// Ruta 404 para endpoints no encontrados
app.use('*', (req, res) => {
  res.status(404).json({
    error: 'Endpoint no encontrado',
    metodo: req.method,
    ruta: req.originalUrl,
  });
});

// Middleware de manejo de errores (siempre al final)
app.use(errorHandler);

module.exports = app;

El archivo src/index.js — punto de entrada:

require('dotenv').config();
const app = require('./app');

const PORT = process.env.PORT || 3000;

app.listen(PORT, () => {
  console.log(`API corriendo en http://localhost:${PORT}`);
  console.log(`Entorno: ${process.env.NODE_ENV}`);
  console.log(`Health check: http://localhost:${PORT}/api/health`);
});

Paso 4: Almacenamiento de datos

Para este tutorial usamos almacenamiento en memoria. En un proyecto real, aquí conectarías MongoDB, PostgreSQL u otra base de datos. La interfaz es la misma:

Archivo src/data/store.js:

const { v4: uuidv4 } = require('uuid');

// "Base de datos" en memoria
let tareas = [
  {
    id: uuidv4(),
    titulo: 'Aprender Node.js',
    descripcion: 'Completar el tutorial de API REST',
    completada: false,
    prioridad: 'alta',
    creadaEn: new Date().toISOString(),
    actualizadaEn: new Date().toISOString(),
  },
];

const store = {
  getAll: () => tareas,
  getById: (id) => tareas.find((t) => t.id === id),
  create: (data) => {
    const nueva = {
      id: uuidv4(),
      ...data,
      completada: false,
      creadaEn: new Date().toISOString(),
      actualizadaEn: new Date().toISOString(),
    };
    tareas.push(nueva);
    return nueva;
  },
  update: (id, data) => {
    const index = tareas.findIndex((t) => t.id === id);
    if (index === -1) return null;
    tareas[index] = {
      ...tareas[index],
      ...data,
      actualizadaEn: new Date().toISOString(),
    };
    return tareas[index];
  },
  delete: (id) => {
    const index = tareas.findIndex((t) => t.id === id);
    if (index === -1) return false;
    tareas.splice(index, 1);
    return true;
  },
};

module.exports = store;

Paso 5: Controlador con lógica de negocio

Archivo src/controllers/tareasController.js:

const store = require('../data/store');

const tareasController = {
  // GET /api/tareas
  listar: (req, res) => {
    let tareas = store.getAll();

    // Filtro por estado
    if (req.query.completada !== undefined) {
      const completada = req.query.completada === 'true';
      tareas = tareas.filter((t) => t.completada === completada);
    }

    // Filtro por prioridad
    if (req.query.prioridad) {
      tareas = tareas.filter((t) => t.prioridad === req.query.prioridad);
    }

    res.json({
      total: tareas.length,
      datos: tareas,
    });
  },

  // GET /api/tareas/:id
  obtener: (req, res) => {
    const tarea = store.getById(req.params.id);
    if (!tarea) {
      return res.status(404).json({ error: 'Tarea no encontrada' });
    }
    res.json(tarea);
  },

  // POST /api/tareas
  crear: (req, res) => {
    const nueva = store.create({
      titulo: req.body.titulo,
      descripcion: req.body.descripcion || '',
      prioridad: req.body.prioridad || 'media',
    });
    res.status(201).json(nueva);
  },

  // PUT /api/tareas/:id
  actualizar: (req, res) => {
    const actualizada = store.update(req.params.id, req.body);
    if (!actualizada) {
      return res.status(404).json({ error: 'Tarea no encontrada' });
    }
    res.json(actualizada);
  },

  // DELETE /api/tareas/:id
  eliminar: (req, res) => {
    const eliminada = store.delete(req.params.id);
    if (!eliminada) {
      return res.status(404).json({ error: 'Tarea no encontrada' });
    }
    res.json({ mensaje: 'Tarea eliminada correctamente' });
  },
};

module.exports = tareasController;

Paso 6: Middleware de validación

Nunca confíes en los datos que llegan del cliente. Este middleware valida los datos antes de que lleguen al controlador.

Archivo src/middleware/validator.js:

const validarTarea = (req, res, next) => {
  const { titulo, prioridad } = req.body;
  const errores = [];

  // Validar título (requerido)
  if (!titulo || typeof titulo !== 'string') {
    errores.push('El título es obligatorio y debe ser texto');
  } else if (titulo.trim().length < 3) {
    errores.push('El título debe tener al menos 3 caracteres');
  } else if (titulo.trim().length > 200) {
    errores.push('El título no puede exceder 200 caracteres');
  }

  // Validar prioridad (opcional, pero si viene debe ser válida)
  const prioridadesValidas = ['baja', 'media', 'alta'];
  if (prioridad && !prioridadesValidas.includes(prioridad)) {
    errores.push(`La prioridad debe ser: ${prioridadesValidas.join(', ')}`);
  }

  if (errores.length > 0) {
    return res.status(400).json({ errores });
  }

  // Sanitizar datos
  req.body.titulo = titulo.trim();
  next();
};

module.exports = { validarTarea };

Paso 7: Middleware de autenticación

Protegemos las rutas de escritura (POST, PUT, DELETE) con una API Key simple. En un proyecto real usarías JWT o OAuth 2.0.

Archivo src/middleware/auth.js:

const autenticar = (req, res, next) => {
  const apiKey = req.headers['x-api-key'];

  if (!apiKey) {
    return res.status(401).json({
      error: 'API Key requerida',
      detalle: 'Incluye el header X-API-KEY en tu petición',
    });
  }

  if (apiKey !== process.env.API_KEY) {
    return res.status(403).json({
      error: 'API Key inválida',
    });
  }

  next();
};

module.exports = autenticar;

Paso 8: Manejo centralizado de errores

En lugar de manejar errores en cada ruta, usamos un middleware central que captura todos los errores no manejados:

Archivo src/middleware/errorHandler.js:

const errorHandler = (err, req, res, next) => {
  console.error(`[ERROR] ${req.method} ${req.path}:`, err.message);

  // Error de JSON malformado
  if (err.type === 'entity.parse.failed') {
    return res.status(400).json({
      error: 'JSON inválido en el body de la petición',
    });
  }

  // Error genérico del servidor
  res.status(err.status || 500).json({
    error: process.env.NODE_ENV === 'production'
      ? 'Error interno del servidor'
      : err.message,
  });
};

module.exports = errorHandler;

Paso 9: Definir las rutas

Archivo src/routes/tareas.js:

const express = require('express');
const router = express.Router();
const tareasController = require('../controllers/tareasController');
const autenticar = require('../middleware/auth');
const { validarTarea } = require('../middleware/validator');

// Rutas públicas (solo lectura)
router.get('/', tareasController.listar);
router.get('/:id', tareasController.obtener);

// Rutas protegidas (escritura - requieren API Key)
router.post('/', autenticar, validarTarea, tareasController.crear);
router.put('/:id', autenticar, validarTarea, tareasController.actualizar);
router.delete('/:id', autenticar, tareasController.eliminar);

module.exports = router;

Paso 10: Probar la API

Inicia el servidor en modo desarrollo:

npm run dev

Ahora prueba cada endpoint con curl desde otra terminal:

# Health check
curl http://localhost:3000/api/health

# Listar todas las tareas
curl http://localhost:3000/api/tareas

# Obtener una tarea por ID (reemplaza el ID)
curl http://localhost:3000/api/tareas/TU-UUID-AQUI

# Crear una tarea (requiere API Key)
curl -X POST http://localhost:3000/api/tareas \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: mi-clave-secreta-2026" \
  -d '{"titulo": "Estudiar Express", "prioridad": "alta"}'

# Actualizar una tarea
curl -X PUT http://localhost:3000/api/tareas/TU-UUID-AQUI \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: mi-clave-secreta-2026" \
  -d '{"titulo": "Estudiar Express (completado)", "completada": true}'

# Eliminar una tarea
curl -X DELETE http://localhost:3000/api/tareas/TU-UUID-AQUI \
  -H "X-API-KEY: mi-clave-secreta-2026"

# Filtrar tareas pendientes
curl "http://localhost:3000/api/tareas?completada=false"

# Filtrar por prioridad
curl "http://localhost:3000/api/tareas?prioridad=alta"

# Probar error de validación
curl -X POST http://localhost:3000/api/tareas \
  -H "Content-Type: application/json" \
  -H "X-API-KEY: mi-clave-secreta-2026" \
  -d '{"titulo": "ab"}'

# Probar error de autenticación
curl -X POST http://localhost:3000/api/tareas \
  -H "Content-Type: application/json" \
  -d '{"titulo": "Sin API Key"}'

Códigos de estado HTTP más usados en APIs

Es importante devolver el código correcto según la especificación HTTP:

CódigoSignificadoCuándo usarlo
200OKLectura o actualización exitosa
201CreatedRecurso creado exitosamente
400Bad RequestDatos de entrada inválidos
401UnauthorizedFalta autenticación
403ForbiddenCredenciales inválidas
404Not FoundRecurso no existe
500Internal Server ErrorError inesperado del servidor

Buenas prácticas para APIs REST

  1. Usa sustantivos en plural para las rutas: /api/tareas (no /api/getTareas o /api/tarea).
  2. Versiona tu API: Cuando tu API crezca, usa versionado: /api/v1/tareas, /api/v2/tareas.
  3. Valida siempre la entrada: Nunca confíes en datos del cliente. Para proyectos más grandes, usa Joi o Zod para validación de esquemas.
  4. Respuestas consistentes: Mantén un formato uniforme — siempre devuelve JSON con la misma estructura de éxito/error.
  5. Logging: En producción usa Winston o Pino en lugar de console.log.
  6. CORS: Si tu frontend está en otro dominio, instala npm install cors y configúralo en Express.
  7. Rate Limiting: Protege tu API de abuso con express-rate-limit.
  8. Documentación: Usa Swagger/OpenAPI para documentar tus endpoints automáticamente.

Próximos pasos

Este tutorial cubre los fundamentos de una API REST profesional. Para llevarlo al siguiente nivel:

  • Base de datos real: Reemplaza el store en memoria con Mongoose (MongoDB) o Prisma (PostgreSQL/MySQL).
  • Autenticación JWT: Implementa registro, login y tokens JWT con jsonwebtoken.
  • Testing: Escribe tests con Jest y Supertest para cada endpoint.
  • Docker: Contenedoriza tu API para deployments consistentes.
  • Deploy: Despliega en Railway, Render o un VPS con Nginx como reverse proxy.

La clave para dominar el desarrollo backend es la práctica. Toma este proyecto base, agrégale funcionalidades nuevas (paginación, búsqueda, uploads) y construye algo propio. Cada problema que resuelvas te acerca a ser un mejor desarrollador.

J
Escrito por
Jesús García

Apasionado por la tecnologia y las finanzas personales. Escribo sobre innovacion, inteligencia artificial, inversiones y estrategias para mejorar tu economia. Mi objetivo es hacer que temas complejos sean accesibles para todos.

Compartir artículo:

Artículos relacionados

Comentarios

Deja un comentario

Herramientas Recomendadas

Las que usamos en nuestros proyectos

Enlaces de afiliado. Sin costo adicional para ti.

¿Necesitas servicios de tecnología?

Ofrecemos soluciones integrales de desarrollo web, aplicaciones móviles, consultoría y más.

Desarrollo Web Apps Móviles Consultoría