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

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

13 min de lectura
95 Vistas
Compartir:

Por qué Node.js y Express siguen siendo relevantes en 2026

Con tantos frameworks surgiendo cada año, una pregunta legítima es si tiene sentido aprender Express en 2026 o si hay alternativas más modernas como Fastify, Hono o Bun. Mi posición es que Express sigue siendo la opción más práctica para la mayoría de equipos latinoamericanos: tiene la mayor cantidad de recursos en español, documentación madura, miles de ejemplos y una comunidad enorme.

Después de construir docenas de APIs con diferentes stacks, lo que más importa no es el framework sino la arquitectura y las prácticas. Y Express es suficientemente flexible para implementar cualquier patrón de diseño que necesites.

Configuración inicial del proyecto

# Crear el proyecto
mkdir mi-api && cd mi-api
npm init -y

# Dependencias de producción
npm install express cors helmet morgan dotenv bcryptjs jsonwebtoken
npm install mongoose  # O: pg, mysql2, prisma según tu base de datos

# Dependencias de desarrollo
npm install -D nodemon eslint prettier

# Estructura del proyecto
mkdir -p src/{routes,controllers,models,middleware,config,utils}
touch src/app.js src/server.js .env .env.example .gitignore
// src/app.js — Configuración central de Express
import express from 'express';
import cors from 'cors';
import helmet from 'helmet';
import morgan from 'morgan';

const app = express();

// Seguridad: headers HTTP seguros
app.use(helmet());

// CORS: permitir peticiones del frontend
app.use(cors({
  origin: process.env.FRONTEND_URL || 'http://localhost:3000',
  credentials: true
}));

// Logging de peticiones
app.use(morgan('dev'));

// Parsear JSON en el body
app.use(express.json({ limit: '10mb' }));
app.use(express.urlencoded({ extended: true }));

// Rutas
import userRoutes from './routes/users.js';
import postRoutes from './routes/posts.js';
import authRoutes from './routes/auth.js';

app.use('/api/v1/auth', authRoutes);
app.use('/api/v1/users', userRoutes);
app.use('/api/v1/posts', postRoutes);

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

// Manejo de rutas no encontradas
app.use('*', (req, res) => {
  res.status(404).json({ error: 'Ruta no encontrada' });
});

// Manejo global de errores
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(err.status || 500).json({
    error: err.message || 'Error interno del servidor'
  });
});

export default app;

Estructura de rutas y controladores

Separar rutas y controladores es una práctica fundamental. Las rutas definen los endpoints; los controladores contienen la lógica. Un error que cometía al principio era poner toda la lógica directamente en las rutas, lo que hacía el código imposible de mantener.

// src/routes/posts.js
import { Router } from 'express';
import { authenticate } from '../middleware/auth.js';
import {
  getAllPosts,
  getPostById,
  createPost,
  updatePost,
  deletePost
} from '../controllers/postController.js';

const router = Router();

// Rutas públicas
router.get('/', getAllPosts);
router.get('/:id', getPostById);

// Rutas protegidas (requieren autenticación)
router.post('/', authenticate, createPost);
router.put('/:id', authenticate, updatePost);
router.delete('/:id', authenticate, deletePost);

export default router;
// src/controllers/postController.js
import Post from '../models/Post.js';

export const getAllPosts = async (req, res) => {
  try {
    const { page = 1, limit = 10, published } = req.query;
    const skip = (page - 1) * limit;

    const filter = {};
    if (published !== undefined) filter.published = published === 'true';

    const [posts, total] = await Promise.all([
      Post.find(filter)
        .populate('author', 'name email')
        .skip(skip)
        .limit(Number(limit))
        .sort({ createdAt: -1 }),
      Post.countDocuments(filter)
    ]);

    res.json({
      data: posts,
      pagination: {
        page: Number(page),
        limit: Number(limit),
        total,
        pages: Math.ceil(total / limit)
      }
    });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

export const createPost = async (req, res) => {
  try {
    const { title, body, tags } = req.body;

    const post = new Post({
      title,
      body,
      tags: tags || [],
      author: req.user.id,
      published: false
    });

    await post.save();
    await post.populate('author', 'name email');

    res.status(201).json({ data: post });
  } catch (error) {
    if (error.code === 11000) {
      return res.status(409).json({ error: 'Ya existe un post con ese slug' });
    }
    res.status(400).json({ error: error.message });
  }
};

Autenticación con JWT

JWT (JSON Web Tokens) es el estándar para autenticación stateless en APIs REST. El flujo es simple: el usuario inicia sesión, el servidor devuelve un token, y el cliente incluye ese token en cada petición protegida.

// src/controllers/authController.js
import bcrypt from 'bcryptjs';
import jwt from 'jsonwebtoken';
import User from '../models/User.js';

export const login = async (req, res) => {
  try {
    const { email, password } = req.body;

    // Verificar que existen los campos
    if (!email || !password) {
      return res.status(400).json({ error: 'Email y contraseña requeridos' });
    }

    // Buscar usuario (incluir password que normalmente está excluido)
    const user = await User.findOne({ email }).select('+password');
    if (!user) {
      return res.status(401).json({ error: 'Credenciales inválidas' });
    }

    // Verificar contraseña
    const isValidPassword = await bcrypt.compare(password, user.password);
    if (!isValidPassword) {
      return res.status(401).json({ error: 'Credenciales inválidas' });
    }

    // Generar token
    const token = jwt.sign(
      { id: user._id, email: user.email, role: user.role },
      process.env.JWT_SECRET,
      { expiresIn: '7d' }
    );

    // No incluir la contraseña en la respuesta
    const userResponse = user.toObject();
    delete userResponse.password;

    res.json({ token, user: userResponse });
  } catch (error) {
    res.status(500).json({ error: error.message });
  }
};

// src/middleware/auth.js
export const authenticate = (req, res, next) => {
  const authHeader = req.headers.authorization;

  if (!authHeader?.startsWith('Bearer ')) {
    return res.status(401).json({ error: 'Token de autenticación requerido' });
  }

  const token = authHeader.split(' ')[1];

  try {
    const decoded = jwt.verify(token, process.env.JWT_SECRET);
    req.user = decoded;
    next();
  } catch (error) {
    if (error.name === 'TokenExpiredError') {
      return res.status(401).json({ error: 'Token expirado, inicia sesión nuevamente' });
    }
    res.status(401).json({ error: 'Token inválido' });
  }
};

Tabla comparativa: frameworks Node.js para APIs

FrameworkVelocidadPopularidadCurva aprendizajeEcosistema
ExpressBuenaMuy altaBajaMaduro y amplio
FastifyExcelenteAlta y creciendoMediaBueno
HonoExcelenteMedia (creciendo)BajaEn desarrollo
NestJSBuenaAlta en empresasAltaMuy completo
KoaBuenaMediaMediaModerado

Validación de datos con Zod

Nunca confíes en los datos que llegan al servidor. La validación debe ocurrir antes de tocar la base de datos.

import { z } from 'zod';  // npm install zod

// Definir esquemas de validación
const createPostSchema = z.object({
  title: z.string().min(5, 'Mínimo 5 caracteres').max(100, 'Máximo 100 caracteres'),
  body: z.string().min(50, 'El contenido debe tener al menos 50 caracteres'),
  tags: z.array(z.string()).max(5, 'Máximo 5 tags').optional(),
  published: z.boolean().optional().default(false),
});

// Middleware de validación reutilizable
const validate = (schema) => (req, res, next) => {
  const result = schema.safeParse(req.body);
  if (!result.success) {
    return res.status(422).json({
      error: 'Datos inválidos',
      details: result.error.errors.map(e => ({
        field: e.path.join('.'),
        message: e.message
      }))
    });
  }
  req.validatedBody = result.data;
  next();
};

// Usar en la ruta
router.post('/', authenticate, validate(createPostSchema), createPost);

Errores comunes y soluciones

Error: Cannot set headers after they are sent to the client

Este error ocurre cuando intentas enviar una respuesta (res.json(), res.send()) más de una vez en el mismo handler. Asegúrate de que cada ruta de código tenga exactamente un return res.xxx(). La solución es agregar return antes de cada res.json() para que la función termine ahí: return res.status(400).json({ error: '...' }).

CORS Error: Access to fetch has been blocked

El frontend no puede comunicarse con la API porque no está en la lista de orígenes permitidos. Verifica que la URL del frontend en cors({ origin: 'URL' }) coincida exactamente, incluyendo el protocolo (http vs https) y el puerto. En desarrollo, http://localhost:3000 y http://localhost:3000/ pueden comportarse diferente en algunos clientes.

MongoServerError: E11000 duplicate key error

Intentas insertar un documento con un valor que ya existe en un campo con índice único (como email o slug). Captura este error específico verificando error.code === 11000 y devuelve un mensaje amigable. Puedes detectar qué campo está duplicado con Object.keys(error.keyPattern)[0].

JsonWebTokenError: invalid signature

El JWT_SECRET en producción difiere del usado al crear el token. Esto pasa cuando cambias la variable de entorno sin invalidar los tokens existentes. Verifica que el JWT_SECRET sea el mismo en el servidor que genera y el que valida tokens. Considera implementar una lista negra de tokens o usar refresh tokens para mayor control.

La API funciona en local pero falla en producción

Las causas más comunes son variables de entorno que no se cargaron en el servidor, una versión diferente de Node.js, o rutas de archivos que asumen el sistema de archivos local. Verifica en producción con node --version, confirma que el .env de producción está cargado correctamente, y revisa los logs del servidor para ver el error real.

Recursos adicionales

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