Tech Tips Monday: 7 API Development Techniques to Speed Up Your Workflow

:wrench: Tech Tips Monday: 7 Técnicas de API Development que Acelerarán tu Workflow

Arrancamos una nueva semana con Tech Tips Monday. Hoy enfoque en desarrollo de APIs: técnicas y herramientas que optimizan el diseño, testing y mantenimiento de servicios robustos.

:high_voltage: 1. OpenAPI Schema-First Development

Diseña tu API antes de escribir código. Define contratos claros que sirvan como documentación y validación automática:

# openapi.yaml
openapi: 3.0.3
info:
  title: User Management API
  version: 1.0.0

paths:
  /users:
    post:
      summary: Create new user
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/UserCreate'
      responses:
        '201':
          description: User created successfully
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/User'

components:
  schemas:
    UserCreate:
      type: object
      required: [email, name]
      properties:
        email:
          type: string
          format: email
        name:
          type: string
          minLength: 2
    User:
      allOf:
        - $ref: '#/components/schemas/UserCreate'
        - type: object
          properties:
            id:
              type: string
              format: uuid
            createdAt:
              type: string
              format: date-time

Generación automática de tipos y validadores:

# Para TypeScript
npx openapi-typescript openapi.yaml --output types/api.ts

# Para validadores Zod
npx openapi-zod-client openapi.yaml --output validators/

Resultado: Documentación siempre actualizada, validación automática, y tipos seguros sin duplicar código.

:bullseye: 2. Request/Response Logging Estructurado

Implementa logging que sea útil para debugging y monitoring:

// middleware/logger.ts
import { Request, Response, NextFunction } from 'express';
import { v4 as uuidv4 } from 'uuid';

interface LogContext {
  requestId: string;
  method: string;
  url: string;
  userAgent?: string;
  userId?: string;
  duration?: number;
  statusCode?: number;
}

export function requestLogger(req: Request, res: Response, next: NextFunction) {
  const requestId = uuidv4();
  const startTime = Date.now();
  
  // Attach requestId to request for use in other middlewares
  req.requestId = requestId;
  
  const logContext: LogContext = {
    requestId,
    method: req.method,
    url: req.originalUrl,
    userAgent: req.get('User-Agent'),
    userId: req.user?.id
  };
  
  // Log request
  console.log('📥 REQUEST', {
    ...logContext,
    body: req.method !== 'GET' ? req.body : undefined,
    query: Object.keys(req.query).length > 0 ? req.query : undefined
  });
  
  // Intercept response
  const originalSend = res.send;
  res.send = function(data) {
    const duration = Date.now() - startTime;
    
    console.log('📤 RESPONSE', {
      ...logContext,
      duration,
      statusCode: res.statusCode,
      responseSize: Buffer.byteLength(data, 'utf8')
    });
    
    // Log errors with stack trace
    if (res.statusCode >= 400) {
      console.error('🚨 ERROR RESPONSE', {
        ...logContext,
        duration,
        statusCode: res.statusCode,
        error: data
      });
    }
    
    return originalSend.call(this, data);
  };
  
  next();
}

Structured logging con Winston:

// config/logger.ts
import winston from 'winston';

export const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  format: winston.format.combine(
    winston.format.timestamp(),
    winston.format.errors({ stack: true }),
    winston.format.json()
  ),
  defaultMeta: { service: 'user-api' },
  transports: [
    new winston.transports.File({ filename: 'logs/error.log', level: 'error' }),
    new winston.transports.File({ filename: 'logs/combined.log' }),
    new winston.transports.Console({
      format: winston.format.combine(
        winston.format.colorize(),
        winston.format.simple()
      )
    })
  ]
});

:magnifying_glass_tilted_left: 3. Rate Limiting Inteligente con Redis

Protege tu API con rate limiting distribuido y configuraciones granulares:

// middleware/rateLimiter.ts
import rateLimit from 'express-rate-limit';
import RedisStore from 'rate-limit-redis';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

// Rate limiting por endpoint
export const apiLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args: string[]) => redis.call(...args),
  }),
  windowMs: 15 * 60 * 1000, // 15 minutos
  max: 100, // máximo 100 requests por ventana
  message: {
    error: 'Too many requests',
    retryAfter: '15 minutes'
  },
  standardHeaders: true,
  legacyHeaders: false,
});

// Rate limiting más estricto para operaciones sensibles
export const authLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args: string[]) => redis.call(...args),
  }),
  windowMs: 15 * 60 * 1000,
  max: 5, // máximo 5 intentos de login por 15 min
  skipSuccessfulRequests: true,
  message: {
    error: 'Too many authentication attempts',
    retryAfter: '15 minutes'
  }
});

// Rate limiting por usuario autenticado
export const userLimiter = rateLimit({
  store: new RedisStore({
    sendCommand: (...args: string[]) => redis.call(...args),
  }),
  windowMs: 60 * 1000, // 1 minuto
  max: 30, // 30 requests per minute por usuario
  keyGenerator: (req) => req.user?.id || req.ip,
  message: {
    error: 'Rate limit exceeded for user',
    retryAfter: '1 minute'
  }
});

Uso en rutas:

// routes/auth.ts
router.post('/login', authLimiter, loginController);
router.post('/register', authLimiter, registerController);

// routes/users.ts  
router.use(apiLimiter); // Rate limiting general
router.get('/', userLimiter, getUsersController);
router.post('/', userLimiter, createUserController);

:shield: 4. Validation Layers con Zod

Implementa validación robusta en múltiples capas:

// schemas/user.ts
import { z } from 'zod';

export const CreateUserSchema = z.object({
  email: z.string()
    .email('Invalid email format')
    .max(100, 'Email too long'),
  name: z.string()
    .min(2, 'Name must be at least 2 characters')
    .max(50, 'Name too long')
    .regex(/^[a-zA-Z\s]+$/, 'Name can only contain letters and spaces'),
  age: z.number()
    .int('Age must be an integer')
    .min(13, 'Must be at least 13 years old')
    .max(120, 'Invalid age'),
  password: z.string()
    .min(8, 'Password must be at least 8 characters')
    .regex(/^(?=.*[a-z])(?=.*[A-Z])(?=.*\d)/, 
           'Password must contain uppercase, lowercase and number'),
  preferences: z.object({
    notifications: z.boolean().default(true),
    theme: z.enum(['light', 'dark']).default('light')
  }).optional()
});

export const UpdateUserSchema = CreateUserSchema.partial().omit({ password: true });

export const UserParamsSchema = z.object({
  id: z.string().uuid('Invalid user ID format')
});

export const UserQuerySchema = z.object({
  page: z.coerce.number().min(1).default(1),
  limit: z.coerce.number().min(1).max(100).default(20),
  search: z.string().optional(),
  sortBy: z.enum(['name', 'email', 'createdAt']).default('createdAt'),
  sortOrder: z.enum(['asc', 'desc']).default('desc')
});

export type CreateUserRequest = z.infer<typeof CreateUserSchema>;
export type UpdateUserRequest = z.infer<typeof UpdateUserSchema>;
export type UserParams = z.infer<typeof UserParamsSchema>;
export type UserQuery = z.infer<typeof UserQuerySchema>;

Middleware de validación reutilizable:

// middleware/validation.ts
import { Request, Response, NextFunction } from 'express';
import { ZodSchema, ZodError } from 'zod';

export function validate(schema: {
  body?: ZodSchema;
  params?: ZodSchema;
  query?: ZodSchema;
}) {
  return async (req: Request, res: Response, next: NextFunction) => {
    try {
      if (schema.body) {
        req.body = await schema.body.parseAsync(req.body);
      }
      
      if (schema.params) {
        req.params = await schema.params.parseAsync(req.params);
      }
      
      if (schema.query) {
        req.query = await schema.query.parseAsync(req.query);
      }
      
      next();
    } catch (error) {
      if (error instanceof ZodError) {
        return res.status(400).json({
          error: 'Validation failed',
          details: error.errors.map(err => ({
            field: err.path.join('.'),
            message: err.message,
            received: err.received
          }))
        });
      }
      next(error);
    }
  };
}

Uso en controladores:

// routes/users.ts
import { validate } from '../middleware/validation';
import { CreateUserSchema, UserParamsSchema, UserQuerySchema } from '../schemas/user';

router.post('/', 
  validate({ body: CreateUserSchema }),
  createUserController
);

router.get('/',
  validate({ query: UserQuerySchema }),
  getUsersController
);

router.get('/:id',
  validate({ params: UserParamsSchema }),
  getUserController
);

:bar_chart: 5. Health Checks Comprensivos

Implementa health checks que realmente informen el estado de tu API:

// routes/health.ts
import { Request, Response } from 'express';
import { PrismaClient } from '@prisma/client';
import Redis from 'ioredis';

const prisma = new PrismaClient();
const redis = new Redis(process.env.REDIS_URL);

interface HealthCheck {
  status: 'healthy' | 'degraded' | 'unhealthy';
  timestamp: string;
  version: string;
  uptime: number;
  checks: {
    database: HealthCheckResult;
    redis: HealthCheckResult;
    external_apis: HealthCheckResult;
    memory: HealthCheckResult;
  };
}

interface HealthCheckResult {
  status: 'pass' | 'fail' | 'warn';
  duration: number;
  details?: any;
}

async function checkDatabase(): Promise<HealthCheckResult> {
  const start = Date.now();
  try {
    await prisma.$queryRaw`SELECT 1`;
    return {
      status: 'pass',
      duration: Date.now() - start
    };
  } catch (error) {
    return {
      status: 'fail',
      duration: Date.now() - start,
      details: error.message
    };
  }
}

async function checkRedis(): Promise<HealthCheckResult> {
  const start = Date.now();
  try {
    await redis.ping();
    return {
      status: 'pass',
      duration: Date.now() - start
    };
  } catch (error) {
    return {
      status: 'fail',
      duration: Date.now() - start,
      details: error.message
    };
  }
}

async function checkExternalAPIs(): Promise<HealthCheckResult> {
  const start = Date.now();
  try {
    // Ejemplo: verificar API externa crítica
    const response = await fetch('https://api.external-service.com/health', {
      timeout: 5000
    });
    
    if (response.ok) {
      return {
        status: 'pass',
        duration: Date.now() - start
      };
    } else {
      return {
        status: 'warn',
        duration: Date.now() - start,
        details: `HTTP ${response.status}`
      };
    }
  } catch (error) {
    return {
      status: 'fail',
      duration: Date.now() - start,
      details: error.message
    };
  }
}

function checkMemory(): HealthCheckResult {
  const memUsage = process.memoryUsage();
  const memUsedMB = memUsage.heapUsed / 1024 / 1024;
  const memLimitMB = 512; // MB
  
  const status = memUsedMB > memLimitMB * 0.9 ? 'warn' : 
                memUsedMB > memLimitMB ? 'fail' : 'pass';
  
  return {
    status,
    duration: 0,
    details: {
      heapUsed: `${memUsedMB.toFixed(2)} MB`,
      heapTotal: `${(memUsage.heapTotal / 1024 / 1024).toFixed(2)} MB`,
      external: `${(memUsage.external / 1024 / 1024).toFixed(2)} MB`
    }
  };
}

export async function healthController(req: Request, res: Response) {
  const startTime = Date.now();
  
  const [database, redis, external_apis] = await Promise.all([
    checkDatabase(),
    checkRedis(),
    checkExternalAPIs()
  ]);
  
  const memory = checkMemory();
  
  const checks = { database, redis, external_apis, memory };
  
  // Determinar status general
  const hasFailures = Object.values(checks).some(check => check.status === 'fail');
  const hasWarnings = Object.values(checks).some(check => check.status === 'warn');
  
  const overallStatus = hasFailures ? 'unhealthy' : 
                       hasWarnings ? 'degraded' : 'healthy';
  
  const healthCheck: HealthCheck = {
    status: overallStatus,
    timestamp: new Date().toISOString(),
    version: process.env.npm_package_version || '1.0.0',
    uptime: process.uptime(),
    checks
  };
  
  const statusCode = overallStatus === 'healthy' ? 200 : 
                    overallStatus === 'degraded' ? 200 : 503;
  
  res.status(statusCode).json(healthCheck);
}

// Health check simple para load balancers
export function readinessController(req: Request, res: Response) {
  res.status(200).json({ status: 'ready' });
}

// Health check básico para monitoreo externo
export function livenessController(req: Request, res: Response) {
  res.status(200).json({ status: 'alive' });
}

:counterclockwise_arrows_button: 6. Response Caching Estratégico

Implementa caching que mejore performance sin comprometer datos frescos:

// middleware/cache.ts
import { Request, Response, NextFunction } from 'express';
import Redis from 'ioredis';

const redis = new Redis(process.env.REDIS_URL);

interface CacheOptions {
  ttl: number; // segundos
  keyPrefix?: string;
  varyBy?: string[]; // headers o query params que afectan cache
  conditionalCache?: (req: Request) => boolean;
}

export function cache(options: CacheOptions) {
  return async (req: Request, res: Response, next: NextFunction) => {
    // Skip cache para métodos no-GET
    if (req.method !== 'GET') {
      return next();
    }
    
    // Cache condicional
    if (options.conditionalCache && !options.conditionalCache(req)) {
      return next();
    }
    
    // Generar cache key
    const keyParts = [
      options.keyPrefix || 'api',
      req.originalUrl,
      req.method
    ];
    
    // Incluir headers/params que afectan la respuesta
    if (options.varyBy) {
      options.varyBy.forEach(vary => {
        const value = req.get(vary) || req.query[vary];
        if (value) keyParts.push(`${vary}:${value}`);
      });
    }
    
    const cacheKey = keyParts.join(':');
    
    try {
      // Buscar en cache
      const cached = await redis.get(cacheKey);
      if (cached) {
        const data = JSON.parse(cached);
        
        // Headers de cache
        res.set('X-Cache', 'HIT');
        res.set('Cache-Control', `public, max-age=${options.ttl}`);
        
        return res.json(data);
      }
      
      // Interceptar response para cachear
      const originalSend = res.send;
      res.send = function(data) {
        // Solo cachear respuestas exitosas
        if (res.statusCode >= 200 && res.statusCode < 300) {
          redis.setex(cacheKey, options.ttl, data);
        }
        
        res.set('X-Cache', 'MISS');
        res.set('Cache-Control', `public, max-age=${options.ttl}`);
        
        return originalSend.call(this, data);
      };
      
      next();
    } catch (error) {
      // Si Redis falla, continuar sin cache
      console.error('Cache error:', error);
      next();
    }
  };
}

// Cache invalidation utilities
export class CacheManager {
  static async invalidatePattern(pattern: string) {
    const keys = await redis.keys(pattern);
    if (keys.length > 0) {
      await redis.del(...keys);
    }
  }
  
  static async invalidateUser(userId: string) {
    await this.invalidatePattern(`api:*user*${userId}*`);
  }
  
  static async invalidateResource(resource: string) {
    await this.invalidatePattern(`api:*/${resource}/*`);
  }
}

Uso estratégico del cache:

// routes/users.ts
import { cache, CacheManager } from '../middleware/cache';

// Cache de lista de usuarios por 5 minutos, varia por paginación
router.get('/', 
  cache({ 
    ttl: 300, 
    keyPrefix: 'users',
    varyBy: ['page', 'limit', 'search']
  }),
  getUsersController
);

// Cache de usuario individual por 10 minutos
router.get('/:id',
  cache({ 
    ttl: 600,
    keyPrefix: 'user'
  }),
  getUserController
);

// Invalidar cache al actualizar
router.put('/:id', 
  updateUserController,
  async (req, res, next) => {
    await CacheManager.invalidateUser(req.params.id);
    await CacheManager.invalidateResource('users');
    next();
  }
);

:bullseye: 7. Error Handling Centralizado

Maneja errores de forma consistente y útil para debugging:

// types/errors.ts
export class APIError extends Error {
  public statusCode: number;
  public code: string;
  public details?: any;
  
  constructor(message: string, statusCode: number, code?: string, details?: any) {
    super(message);
    this.name = 'APIError';
    this.statusCode = statusCode;
    this.code = code || 'GENERIC_ERROR';
    this.details = details;
    
    Error.captureStackTrace(this, this.constructor);
  }
}

export class ValidationError extends APIError {
  constructor(message: string, details?: any) {
    super(message, 400, 'VALIDATION_ERROR', details);
  }
}

export class NotFoundError extends APIError {
  constructor(resource: string, id?: string) {
    const message = id ? `${resource} with id ${id} not found` : `${resource} not found`;
    super(message, 404, 'NOT_FOUND');
  }
}

export class UnauthorizedError extends APIError {
  constructor(message: string = 'Unauthorized') {
    super(message, 401, 'UNAUTHORIZED');
  }
}

export class ForbiddenError extends APIError {
  constructor(message: string = 'Forbidden') {
    super(message, 403, 'FORBIDDEN');
  }
}

export class ConflictError extends APIError {
  constructor(message: string, details?: any) {
    super(message, 409, 'CONFLICT', details);
  }
}

Error handler centralizado:

// middleware/errorHandler.ts
import { Request, Response, NextFunction } from 'express';
import { APIError } from '../types/errors';
import { logger } from '../config/logger';

export function errorHandler(
  error: Error,
  req: Request,
  res: Response,
  next: NextFunction
) {
  // Log del error
  const logContext = {
    requestId: req.requestId,
    method: req.method,
    url: req.originalUrl,
    userAgent: req.get('User-Agent'),
    userId: req.user?.id,
    error: {
      name: error.name,
      message: error.message,
      stack: error.stack
    }
  };
  
  if (error instanceof APIError) {
    logger.warn('API Error', logContext);
    
    return res.status(error.statusCode).json({
      error: {
        message: error.message,
        code: error.code,
        details: error.details,
        requestId: req.requestId
      }
    });
  }
  
  // Errores de validación de Zod (si no fueron capturados)
  if (error.name === 'ZodError') {
    logger.warn('Validation Error', logContext);
    
    return res.status(400).json({
      error: {
        message: 'Validation failed',
        code: 'VALIDATION_ERROR',
        details: error.errors,
        requestId: req.requestId
      }
    });
  }
  
  // Errores de base de datos
  if (error.name === 'PrismaClientKnownRequestError') {
    logger.error('Database Error', logContext);
    
    // Errores comunes de Prisma
    if (error.code === 'P2002') {
      return res.status(409).json({
        error: {
          message: 'Resource already exists',
          code: 'DUPLICATE_ENTRY',
          requestId: req.requestId
        }
      });
    }
    
    if (error.code === 'P2025') {
      return res.status(404).json({
        error: {
          message: 'Resource not found',
          code: 'NOT_FOUND',
          requestId: req.requestId
        }
      });
    }
  }
  
  // Error genérico - no exponer detalles internos
  logger.error('Unhandled Error', logContext);
  
  res.status(500).json({
    error: {
      message: 'Internal server error',
      code: 'INTERNAL_ERROR',
      requestId: req.requestId
    }
  });
}

// 404 handler
export function notFoundHandler(req: Request, res: Response) {
  res.status(404).json({
    error: {
      message: `Route ${req.method} ${req.originalUrl} not found`,
      code: 'ROUTE_NOT_FOUND',
      requestId: req.requestId
    }
  });
}

Uso en controllers:

// controllers/userController.ts
import { Request, Response, NextFunction } from 'express';
import { NotFoundError, ConflictError } from '../types/errors';

export async function createUserController(
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    const { email, name, password } = req.body;
    
    // Verificar si usuario existe
    const existingUser = await prisma.user.findUnique({ where: { email } });
    if (existingUser) {
      throw new ConflictError('User with this email already exists');
    }
    
    const user = await prisma.user.create({
      data: { email, name, password: await hashPassword(password) },
      select: { id: true, email: true, name: true, createdAt: true }
    });
    
    res.status(201).json({ user });
  } catch (error) {
    next(error);
  }
}

export async function getUserController(
  req: Request,
  res: Response,
  next: NextFunction
) {
  try {
    const { id } = req.params;
    
    const user = await prisma.user.findUnique({
      where: { id },
      select: { id: true, email: true, name: true, createdAt: true }
    });
    
    if (!user) {
      throw new NotFoundError('User', id);
    }
    
    res.json({ user });
  } catch (error) {
    next(error);
  }
}

:light_bulb: Configuración Integral: Setup Completo

Script de configuración automática:

#!/bin/bash
# setup-api-project.sh

echo "🔧 Configurando proyecto API con best practices..."

# Instalar dependencias
npm install express cors helmet morgan compression
npm install -D @types/express @types/cors typescript ts-node nodemon

# Dependencias adicionales
npm install zod redis ioredis express-rate-limit prisma @prisma/client
npm install winston express-slow-down jsonwebtoken bcryptjs

# Crear estructura
mkdir -p {src/{controllers,middleware,routes,schemas,types,config},tests,logs}

# Configurar TypeScript
cat > tsconfig.json << EOF
{
  "compilerOptions": {
    "target": "ES2020",
    "module": "commonjs",
    "lib": ["ES2020"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "resolveJsonModule": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist", "tests"]
}
EOF

# Scripts de package.json
npm pkg set scripts.dev="nodemon src/index.ts"
npm pkg set scripts.build="tsc"
npm pkg set scripts.start="node dist/index.js"
npm pkg set scripts.test="jest"
npm pkg set scripts.lint="eslint src/**/*.ts"

echo "✅ Proyecto API configurado con best practices!"
echo "📁 Estructura creada en: $(pwd)"
echo "🚀 Próximos pasos:"
echo "  1. Configurar variables de entorno (.env)"
echo "  2. Configurar base de datos (Prisma schema)"
echo "  3. Implementar rutas según OpenAPI spec"

:bullseye: Plan de Implementación Semanal

Semana 1: Fundamentos

  • Configurar OpenAPI schema para tu API principal
  • Implementar logging estructurado con requestId
  • Configurar validación con Zod en 3 endpoints críticos

Semana 2: Seguridad y Performance

  • Implementar rate limiting en endpoints sensibles
  • Configurar health checks comprensivos
  • Agregar caching estratégico a endpoints de lectura

Semana 3: Robustez

  • Centralizar error handling con tipos específicos
  • Configurar monitoring y alertas basadas en health checks
  • Implementar cache invalidation automática

Semana 4: Optimización

  • Analizar logs para optimizar performance
  • Refinar rate limiting basado en patrones de uso real
  • Documentar patrones para el equipo

:bar_chart: Métricas de Éxito

Antes de implementar:

  • Tiempo promedio de debugging: ~45min por issue
  • APIs sin documentación actualizada: ~60%
  • Errores sin contexto útil: ~40%
  • Rate limiting básico o inexistente

Después de implementar:

  • Tiempo promedio de debugging: ~15min por issue (-67%)
  • APIs con documentación auto-generada: 100%
  • Errores con contexto estructurado: 100%
  • Rate limiting granular y monitoring en tiempo real

:speech_balloon: Desafío de la Semana

Implementa al menos 3 de estas técnicas en tu API actual:

  1. OpenAPI Schema-First: Define schema para un endpoint y genera tipos automáticamente
  2. Logging Estructurado: Agrega requestId y contexto útil a todos los logs
  3. Validación Robusta: Implementa Zod schemas con mensajes de error claros
  4. Rate Limiting: Protege endpoints críticos con límites apropiados
  5. Health Checks: Implementa checks que realmente informen el estado del sistema

Bonus: Configura monitoring automático basado en health checks y comparte las métricas de performance antes/después.

Las APIs robustas no se construyen por accidente - requieren patrones consistentes, herramientas apropiadas, y atención a detalles que marcan la diferencia entre un servicio que funciona y uno que funciona excelentemente en producción.

¿Cuál de estas técnicas ha tenido más impacto en la robustez de sus APIs? ¿Qué patterns de API development consideran indispensables?

techtipsmonday apidesign backenddevelopment nodejs typescript #APITesting performance security