Saltar al contenido
home / blog / api-rest-nodejs-express
Node.js Express API

Construyendo una API REST robusta con Node.js y Express

2026-04-12 · 15 min de lectura
Construyendo una API REST robusta con Node.js y Express

Desde cero hasta producción: autenticación, validación, manejo de errores y buenas prácticas para APIs modernas.

Estructura del proyecto

Una API bien estructurada es mantenible a largo plazo. Esta es la organización que uso en proyectos reales:

src/
  routes/          → Definición de endpoints
  controllers/     → Lógica de cada endpoint
  services/        → Lógica de negocio
  middleware/      → Auth, validación, errores
  models/          → Esquemas de BD
  lib/             → DB connection, config
  types/           → Tipos TypeScript

La separación controller → service es clave: el controller maneja HTTP, el service maneja la lógica.

Setup inicial

// src/app.ts
import express from 'express';
import helmet from 'helmet';
import cors from 'cors';
import { errorHandler } from './middleware/errorHandler';
import { router } from './routes';

const app = express();

app.use(helmet());
app.use(cors({ origin: process.env.ALLOWED_ORIGINS?.split(',') }));
app.use(express.json({ limit: '10kb' }));

app.use('/api/v1', router);
app.use(errorHandler); // Siempre al final

export { app };

Validación de requests

Usa zod para validar el body antes de que llegue al controller:

// src/middleware/validate.ts
import { z, ZodSchema } from 'zod';
import { Request, Response, NextFunction } from 'express';

export function validate(schema: ZodSchema) {
  return (req: Request, res: Response, next: NextFunction) => {
    const result = schema.safeParse(req.body);
    if (!result.success) {
      return res.status(400).json({
        error: 'Validation failed',
        details: result.error.flatten().fieldErrors,
      });
    }
    req.body = result.data;
    next();
  };
}

// Uso en la ruta:
const createUserSchema = z.object({
  email: z.string().email(),
  password: z.string().min(8),
  name: z.string().min(2).max(50),
});

router.post('/users', validate(createUserSchema), userController.create);

Manejo de errores centralizado

Define clases de error propias y un handler global:

// src/lib/errors.ts
export class AppError extends Error {
  constructor(
    public message: string,
    public statusCode: number,
    public code?: string
  ) {
    super(message);
  }
}

export class NotFoundError extends AppError {
  constructor(resource: string) {
    super(`${resource} not found`, 404, 'NOT_FOUND');
  }
}

// src/middleware/errorHandler.ts
export function errorHandler(
  err: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  if (err instanceof AppError) {
    return res.status(err.statusCode).json({
      error: err.message,
      code: err.code,
    });
  }

  console.error('Unexpected error:', err);
  res.status(500).json({ error: 'Internal server error' });
}

Ahora en cualquier controller puedes hacer throw new NotFoundError('User') y el handler lo captura.

Autenticación con JWT

// src/middleware/auth.ts
import jwt from 'jsonwebtoken';

export function requireAuth(req: Request, res: Response, next: NextFunction) {
  const token = req.headers.authorization?.replace('Bearer ', '');
  
  if (!token) {
    throw new AppError('No token provided', 401, 'UNAUTHORIZED');
  }

  try {
    const payload = jwt.verify(token, process.env.JWT_SECRET!) as JwtPayload;
    req.user = payload;
    next();
  } catch {
    throw new AppError('Invalid token', 401, 'INVALID_TOKEN');
  }
}

Rate limiting

Protege endpoints sensibles:

import rateLimit from 'express-rate-limit';

export const authLimiter = rateLimit({
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 10,
  message: { error: 'Too many attempts, try again later' },
});

router.post('/auth/login', authLimiter, authController.login);

Respuestas consistentes

Define un formato fijo para todas las respuestas:

// src/lib/response.ts
export function success<T>(res: Response, data: T, status = 200) {
  return res.status(status).json({ success: true, data });
}

export function paginated<T>(res: Response, data: T[], meta: PaginationMeta) {
  return res.json({ success: true, data, meta });
}

Conclusión

Una API robusta no es solo código que funciona — es código que falla de forma predecible, valida en los bordes, y es legible para el siguiente dev. Los patrones de este artículo (validación con Zod, errores tipados, respuestas consistentes) son los que marcan la diferencia entre un prototipo y código de producción.