Cómo hacer cualquier cosa en programación: del criterio al código
Hay una pregunta que parece demasiado grande para responderla bien:
¿Cómo se hace cualquier cosa en programación?
No “cómo se hace un botón”, “cómo se conecta una API” o “cómo se guarda algo en una base de datos”. Eso se puede buscar. La pregunta difícil es otra: cómo piensas el proceso completo para pasar de una idea borrosa a algo que funciona, se entiende, se puede probar y no se rompe al primer cambio.
La respuesta corta sería: paso a paso.
La respuesta útil es esta: primero entiendes el problema, luego reduces el alcance, después diseñas una estructura mínima, haces funcionar el camino principal, añades seguridad y errores desde el principio, pruebas, refactorizas y solo entonces publicas.
No es magia. Es oficio.
1. Antes de programar: definir qué significa “funciona”
El error más común es abrir el editor demasiado pronto. Programar sin definir el resultado esperado es como construir una casa diciendo “ya veremos dónde van las paredes”.
Antes de escribir código, tienes que responder tres preguntas:
- Qué quiere hacer el usuario
- Qué datos necesita el sistema
- Qué significa que esté terminado
Ejemplo: “quiero un CRUD de tareas” suena claro, pero no lo es.
Un CRUD real necesita decisiones:
- ¿Una tarea tiene título, descripción, estado, fecha límite?
- ¿Puede borrarse cualquier tarea?
- ¿Hay usuarios o todas las tareas son públicas?
- ¿Qué pasa si el título llega vacío?
- ¿Qué devuelve la API si la tarea no existe?
- ¿Dónde se guarda la información?
- ¿Cómo pruebo que todo sigue funcionando mañana?
Hasta que no respondes eso, no estás programando. Estás adivinando.
2. Reducir el problema: la primera versión no es la versión final
La primera versión debe resolver el camino principal, no todos los casos imaginables.
Para un CRUD de tareas, el alcance mínimo puede ser:
- crear una tarea
- listar tareas
- ver una tarea por id
- actualizar una tarea
- borrar una tarea
Y nada más.
No autenticación con Google. No roles. No subida de archivos. No notificaciones por email. No dashboard con gráficos. Todo eso puede venir después si hace falta.
Esto no es hacer las cosas mal. Es hacerlas en orden.
Una buena primera versión responde a esta pregunta:
¿Cuál es el flujo más pequeño que demuestra que la idea funciona?
En Clean Code, Robert C. Martin insiste mucho en funciones pequeñas, nombres claros y código legible. Eso ayuda. Pero antes de llegar a esa capa hay un principio más básico: no compliques el problema antes de entenderlo.
3. Diseñar la estructura mínima
La estructura no tiene que ser perfecta. Tiene que ser entendible.
Para un backend pequeño con Node, Express y una base de datos, una estructura razonable puede ser:
src/
server.ts # arranque del servidor
routes/
tasks.routes.ts # endpoints HTTP
controllers/
tasks.controller.ts
services/
tasks.service.ts # reglas de negocio
repositories/
tasks.repo.ts # acceso a base de datos
db/
client.ts
schemas/
task.schema.ts # validación de entrada
errors/
AppError.ts
No necesitas veinte capas para empezar. Pero sí conviene separar responsabilidades:
- route: define la URL y el verbo HTTP
- controller: traduce request/response
- service: contiene la lógica del caso de uso
- repository: habla con la base de datos
- schema: valida lo que entra
- error: normaliza fallos
La idea es simple: si mañana cambias Express por otro framework, la lógica no debería estar pegada a req y res por todo el proyecto. Y si mañana cambias PostgreSQL por otra cosa, no deberías reescribir toda la aplicación.
4. Hacer una función que funcione, pero con criterio
Una buena función no es la más lista. Es la que se entiende y hace una cosa concreta.
Mala señal:
async function handleTask(req, res) {
// valida, calcula, consulta la base de datos,
// decide estados HTTP, formatea fechas y maneja errores
}
Mejor:
async function createTask(input: CreateTaskInput) {
const task = validateCreateTask(input);
return tasksRepository.create({
title: task.title,
description: task.description ?? null,
status: 'pending',
});
}
La función createTask no sabe de Express. No sabe de HTML. No sabe de cookies. Sabe crear una tarea válida.
Ese es el criterio: cada pieza debería tener un motivo claro para cambiar.
Si cambia la validación, tocas el schema.
Si cambia la base de datos, tocas el repository.
Si cambia el formato HTTP, tocas el controller.
5. Seguridad: no se añade al final
La seguridad no es una fase final tipo “ya lo miraré antes de subirlo”. Si la dejas para el final, ya has construido el proyecto con supuestos inseguros.
Desde el primer endpoint hay que pensar en:
- validación de entrada: no confíes en
req.body - secretos fuera del código:
.env, nunca claves hardcodeadas - mensajes de error seguros: no devuelvas trazas internas al usuario
- consultas parametrizadas u ORM: evita concatenar SQL manualmente
- límites básicos: tamaño del payload, rate limit si aplica
- permisos: si hay usuarios, cada operación debe comprobar quién puede hacer qué
Ejemplo de validación:
const createTaskSchema = z.object({
title: z.string().trim().min(1).max(120),
description: z.string().trim().max(1000).optional(),
});
Esto parece aburrido. Perfecto. La seguridad buena suele ser aburrida porque evita que pasen cosas interesantes en producción.
6. Control de errores: decidir qué pasa cuando algo falla
Todo código real falla.
Falla porque el usuario manda datos incorrectos. Falla porque la base de datos no responde. Falla porque una tarea no existe. Falla porque una variable de entorno no está configurada. Falla porque tú, como todo el mundo, te equivocaste.
Lo profesional no es fingir que no fallará. Es diseñar qué pasa cuando falle.
Un patrón sencillo:
export class AppError extends Error {
constructor(
public statusCode: number,
message: string
) {
super(message);
}
}
Uso:
async function getTaskById(id: string) {
const task = await tasksRepository.findById(id);
if (!task) {
throw new AppError(404, 'Tarea no encontrada');
}
return task;
}
Y un middleware global:
app.use((error, req, res, next) => {
if (error instanceof AppError) {
return res.status(error.statusCode).json({ error: error.message });
}
console.error(error);
return res.status(500).json({ error: 'Error interno del servidor' });
});
El objetivo no es capturar errores por capturarlos. Es evitar respuestas inconsistentes y filtrar información interna.
7. Base de datos: local, Docker, staging o producción
Aquí hay mucha duda razonable: ¿monto la base de datos en mi ordenador? ¿Uso Docker? ¿Mejor un servicio gratuito en la nube? ¿Trabajo directamente contra producción?
La respuesta práctica:
Producción nunca debería ser tu entorno de pruebas.
Trabajar directamente contra producción es cómodo hasta que borras datos reales, rompes una migración o llenas la base de registros basura.
Opción 1: base de datos local
Buena para aprender y empezar rápido.
Ventajas:
- no dependes de internet
- es rápida
- controlas todo
- puedes romperla sin miedo
Problemas:
- tu entorno puede no parecerse a producción
- cuesta compartirlo con otras personas
- configurar PostgreSQL, MySQL o Mongo local puede ser tedioso
Opción 2: Docker
Para la mayoría de proyectos backend, Docker es una muy buena opción.
services:
db:
image: postgres:16
ports:
- '5432:5432'
environment:
POSTGRES_USER: app
POSTGRES_PASSWORD: app
POSTGRES_DB: tasks
Ventajas:
- todos usan la misma versión
- se puede levantar y tirar el entorno fácilmente
- se parece más a un entorno real
- no ensucia tanto tu sistema operativo
Problemas:
- hay que aprender Docker lo justo
- consume algo más de recursos
Opción 3: servicio gratuito en la nube
Supabase, Neon, MongoDB Atlas, Turso y otros servicios tienen planes gratuitos útiles para prototipos.
Ventajas:
- te acercas más a un entorno real
- no instalas base de datos local
- puedes probar desde varios dispositivos o con otras personas
Problemas:
- dependes de red y límites del plan gratuito
- tienes que cuidar credenciales
- si usas una sola base para todo, puedes mezclar pruebas y datos reales
Entonces, ¿qué haría yo?
Para un CRUD backend:
- Desarrollo local con Docker para programar rápido y romper sin miedo.
- Base de datos de staging/test en un servicio gratuito para probar despliegue, variables de entorno, latencia y migraciones.
- Producción separada solo para datos reales.
La clave no es “local o nube”. La clave es separar entornos:
local -> mi máquina, datos descartables
staging -> nube de pruebas, parecido a producción
production -> datos reales, cuidado máximo
Tu ordenador sí puede funcionar perfectamente como entorno de desarrollo. De hecho, debe poder hacerlo. Pero eso no significa que sea el único sitio donde probar antes de publicar.
8. Construir el CRUD por partes
No implementes todo a la vez. Haz una ruta completa de principio a fin y luego repite el patrón.
Orden recomendado:
- Conexión a la base de datos
- Modelo o tabla
tasks - Crear tarea
- Listar tareas
- Ver tarea por id
- Actualizar tarea
- Borrar tarea
- Validaciones
- Errores comunes
- Tests
Primero haz que POST /tasks funcione bien. Con validación, error si falta el título y guardado real.
Luego GET /tasks.
Luego GET /tasks/:id.
No saltes entre cinco endpoints a la vez. El progreso real no es tener muchos archivos abiertos. Es tener una parte completa funcionando.
9. Probar: no solo “lo he mirado en Postman”
Postman, Insomnia o Thunder Client están bien para probar manualmente. Pero no bastan.
Pruebas mínimas para el CRUD:
- crear una tarea válida devuelve
201 - crear una tarea sin título devuelve
400 - listar tareas devuelve un array
- pedir una tarea inexistente devuelve
404 - actualizar una tarea inexistente devuelve
404 - borrar una tarea existente devuelve
204o una respuesta clara
También conviene probar casos feos:
- ids mal formados
- payloads demasiado grandes
- campos extraños
- base de datos no disponible
- variables de entorno ausentes
No hace falta convertir cada proyecto pequeño en una catedral de testing. Pero sí necesitas pruebas suficientes para responder:
Si cambio esto mañana, ¿cómo sé que no he roto lo básico?
10. Refactorizar cuando ya existe comportamiento
Refactorizar antes de tener algo que funcione suele ser fantasía arquitectónica.
Primero haz que funcione.
Luego hazlo claro.
Después hazlo más robusto.
Ese orden importa.
Pero “hacer que funcione” no significa escribir basura. Significa aceptar que la primera versión será imperfecta y que la mejorarás con más información.
Un buen refactor suele buscar:
- nombres más claros
- funciones más pequeñas
- menos duplicación real
- responsabilidades mejor separadas
- errores más consistentes
- tests que cubran el comportamiento importante
Clean Code ayuda aquí, pero con una advertencia: no conviertas cada principio en religión. La buena arquitectura no es la que cumple frases bonitas. Es la que hace más fácil cambiar el sistema sin romperlo.
11. Checklist antes de publicar
Antes de desplegar, revisa:
.envno está subido al repositorio.env.examplesí está actualizado- las migraciones de base de datos se ejecutan correctamente
- producción y staging usan bases de datos separadas
- los errores internos no se muestran al usuario
- los endpoints validan entrada
- no hay claves hardcodeadas
- hay logs suficientes para diagnosticar fallos
- el build pasa
- las pruebas importantes pasan
- tienes una forma de rollback si algo sale mal
No todo proyecto necesita Kubernetes, CI/CD complejo y observabilidad avanzada.
Pero todo proyecto serio necesita una idea básica de:
- cómo se configura
- cómo se prueba
- cómo se publica
- cómo se recupera si falla
El método completo
Si tuviera que resumirlo en una receta, sería esta:
- Define el resultado esperado.
- Reduce el alcance a la primera versión útil.
- Diseña una estructura mínima y entendible.
- Implementa un flujo completo de principio a fin.
- Valida datos desde el primer endpoint.
- Controla errores de forma consistente.
- Usa local o Docker para desarrollar sin miedo.
- Usa staging para probar despliegue en un entorno realista.
- Reserva producción para datos reales.
- Añade tests sobre lo importante.
- Refactoriza con comportamiento ya comprobado.
- Publica con checklist, no con fe.
Programar no es solo escribir instrucciones para la máquina. Es tomar decisiones en orden.
La parte difícil no es saber crear una función. Es saber qué función toca crear ahora, qué responsabilidad debe tener, qué puede fallar, cómo lo vas a probar y cuándo la solución ya es suficientemente buena para avanzar.
Eso es construir software con criterio.