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.
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.
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()
)
})
]
});
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);
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
);
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' });
}
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();
}
);
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);
}
}
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"
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
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
Desafío de la Semana
Implementa al menos 3 de estas técnicas en tu API actual:
- OpenAPI Schema-First: Define schema para un endpoint y genera tipos automáticamente
- Logging Estructurado: Agrega requestId y contexto útil a todos los logs
- Validación Robusta: Implementa Zod schemas con mensajes de error claros
- Rate Limiting: Protege endpoints críticos con límites apropiados
- 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
