16  7. Necesitamos que esto quepa en la cabeza de otra persona

Un archivo de 800 líneas con 30 endpoints no es un proyecto

NotaEn una frase

Vas a organizar el código en capas — transporte, negocio, datos, contratos — para que cada archivo haga una sola cosa y la llegada de la base de datos (Parte 2) no rompa nada. Es el mismo components/services/ hooks que ya usas en frontend, pero del lado servidor.

16.1 El problema

Taskflow ya tiene 12 endpoints, schemas, errores de dominio, la lista en memoria… todo en un archivo que empieza a dar miedo. Cuando llegue la base de datos (Parte 2), la autenticación (Parte 3) y los tests (Parte 6), ese archivo será inmantenible.

Un array es una colección ordenada de elementos accedidos por posición: ["a","b","c"][0] es "a" (se cuenta desde 0). Python las llama listas, Go slices — misma idea, distinto acento.

La memoria RAM es el escritorio de trabajo del programa: rápida, pero se borra al apagar. Los datos que deben sobrevivir van a disco o a la base de datos. Por eso let tasks = [] pierde todo al reiniciar.

La autenticación responde “¿quién eres?” — login, contraseña, token. Se confunde con autorización (“¿qué puedes hacer?”), que es la pregunta siguiente. Primero te identificas, luego te dejan o no pasar.

Pero el problema real no es el tamaño — es la mezcla: en la misma función convive “leer el path param” (HTTP), “la tarea no existe” (negocio) y “meterla en la lista” (datos). Si mañana el storage cambia de lista a Postgres, tocas cada handler. Si quieres testear la lógica sin levantar el servidor, no puedes — la lógica está pegada a HTTP.

16.2 Cómo lo resuelve un equipo

La decisión arquitectónica más común y más útil del backend: capas. Cada capa tiene un trabajo y solo habla con su vecina:

┌─────────────────────────────────────────────────┐
│ routes/handlers   →  HTTP ↔ lenguaje: verbos,    │
│                      params, status codes        │
├─────────────────────────────────────────────────┤
│ services          →  negocio: reglas, errores    │
│                      de dominio                  │
├─────────────────────────────────────────────────┤
│ storage/models    →  datos: queries, persistencia│
├─────────────────────────────────────────────────┤
│ schemas/contracts →  contratos: lo que entra     │
│                      y lo que sale (Cap. 4 y 5)  │
└─────────────────────────────────────────────────┘

La regla de oro que hace funcionar todo: los handlers son tontos. createTask en la ruta solo traduce HTTP → llamada al servicio → HTTP. La regla “no borres una tarea con subtareas” vive en el servicio — así se puede llamar desde otro endpoint, un test o un WebSocket sin duplicar.

El mismo endpoint, antes y después:

# ANTES — handler gordo: transporte + negocio + datos mezclados
@app.delete("/tasks/{task_id}")
def delete(task_id: int):
    task = next((t for t in tasks if t.id == task_id), None)
    if not task: raise HTTPException(404)
    if any(s for s in subtasks if s.task_id == task_id):
        raise HTTPException(409, "has subtasks")      # regla en la ruta
    tasks.remove(task)

# DESPUÉS — handler tonto: la regla vive en el servicio
@router.delete("/{task_id}", status_code=204)
def delete(task_id: int):
    svc.delete_task(task_id)    # TaskNotFound/Conflict salen del servicio

La prueba de que lo hiciste bien llega en la Parte 6: los tests de servicio no necesitan ni levantar HTTP.

16.3 Conceptos nuevos

  • U Arquitectura por capas: transporte / negocio / datos / contratos — y por qué cada capa ignora a las demás.
  • TS ES modules + routers de Express: import/export, express.Router() para separar rutas por dominio.
  • Py Módulos y paquetes: import, from x import y, __init__.py — el equivalente a ES modules con sus trampas; APIRouter para agrupar endpoints.
  • Go Packages: un directorio = un package; nombres cortos; internal/ como frontera que el compilador

Un compilador traduce código a otra forma: TypeScript → JavaScript (transpila), Go → binario (compila). Atrapa errores antes de ejecutar. tsc, esbuild, go build son compiladores.

hace cumplir. - U Lógica reusable pre-handler: middleware (Express) vs Depends (FastAPI) vs func(http.Handler) http.Handler (Go) — el mismo patrón, tres cantidades de magia. Primer contacto; el Cap. 16 lo exprime.

Una función es una receta reutilizable: recibe ingredientes (parámetros), hace pasos y devuelve un plato (return). La escribes una vez y la llamas mil veces: add(2, 3) → 5.

16.4 La explicación visual

flowchart TB
  subgraph app["taskflow/"]
    direction TB
    R["routes/tasks.ts · api/tasks.py · httpapi/tasks.go<br/><b>transporte</b>: HTTP ↔ tipos del lenguaje"]
    S["services/taskService.ts · services/task_service.py · service/task.go<br/><b>negocio</b>: reglas, errores de dominio"]
    ST["storage/memory.ts · store/memory.py · store/memory.go<br/><b>datos</b>: hoy lista; mañana Postgres"]
    SC["schemas/ · dto/ <br/><b>contratos</b>: TaskCreate, TaskPublic"]
    R --> S --> ST
    R -.->|"usa para entrada/salida"| SC
  end
  Client["Cliente"] -->|"HTTP"| R

Cuando llegue Postgres (Cap. 10), solo storage/ cambia. Cuando llegue auth (Cap. 16), se cuelga una dependencia antes del handler. Las features nuevas se agregan sin reescribir — esa es la definición operativa de “buena arquitectura”.

Una dependencia es código de terceros que tu proyecto usa: npm install, pip install, go get las traen. Cada una es deuda — ahora funciona, pero hay que mantenerla, actualizarla y confiar en ella.

16.5 Implementación

El refactor del Cap. 3–6 a capas, en los tres stacks:

src/
├── index.ts              # ensambla: app + json + routers + error middleware
├── routes/tasks.ts       # transporte: verbos, params, status
├── services/tasks.ts     # negocio: reglas, DomainErrors
├── storage/memory.ts     # datos: la lista (mañana: Postgres)
└── schemas/tasks.ts      # contratos: zod + toPublic
// routes/tasks.ts — el handler tonto
import { Router } from "express";
import * as svc from "../services/tasks.js";
import { createTaskSchema } from "../schemas/tasks.js";

export const tasksRouter = Router();

tasksRouter.post("/", (req, res, next) => {
  const parsed = createTaskSchema.safeParse(req.body);
  if (!parsed.success) return res.status(422).json(/* … */);
  try {
    res.status(201).json(svc.createTask(parsed.data));
  } catch (e) { next(e); }
});
// index.ts — el ensamblador
import express from "express";
import { tasksRouter } from "./routes/tasks.js";
import { errorMiddleware } from "./errors.js";

const app = express();
app.use(express.json());
app.use("/tasks", tasksRouter);
app.use(errorMiddleware);
app.listen(8000);
// NestJS hace las capas por ti: module → controller → service con DI
@Module({
  controllers: [TasksController],
  providers: [TasksService],   // el servicio se inyecta solo
})
export class TasksModule {}

@Controller("tasks")
export class TasksController {
  constructor(private svc: TasksService) {}  // DI declarativa
  @Post() create(@Body() dto: CreateTaskDto) { return this.svc.create(dto); }
}

Qué te cuesta: la arquitectura viene decidida (módulos, decoradores, guards, interceptors — estilo Angular en el backend). Ganas convenciones fuertes y DI gratis; pagas con magia, build más pesado y “the NestJS way”. En Express tú eres el arquitecto; en NestJS lo es el framework.

app/
├── main.py            # ensambla: FastAPI + routers + exception handlers
├── api/tasks.py       # transporte
├── services/tasks.py  # negocio
├── store/memory.py    # datos
├── schemas/tasks.py   # contratos: TaskCreate, TaskPublic
└── errors.py          # DomainError + handlers
# api/tasks.py — el endpoint delgado
from fastapi import APIRouter
from app.schemas.tasks import TaskCreate, TaskPublic
from app.services import tasks as svc

router = APIRouter(prefix="/tasks")

@router.post("", status_code=201, response_model=TaskPublic)
def create_task(payload: TaskCreate):
    return svc.create_task(payload)
# main.py — el ensamblador
from fastapi import FastAPI
from app.api.tasks import router as tasks_router
from app.errors import register_handlers

app = FastAPI()
app.include_router(tasks_router)
register_handlers(app)
# Django divide el proyecto en "apps" — capas predefinidas por dominio
taskflow/
├── tasks/                  # app por dominio
│   ├── models.py           # datos (ORM integrado)
│   ├── views.py            # transporte
│   ├── serializers.py      # contratos (DRF)
│   └── services.py         # negocio (convención, no impuesta)

Qué te cuesta: Django decide la estructura por ti (models/views/serializers por app) — aprendes un sistema completo a cambio de que todo esté resuelto (ORM, admin, auth). FastAPI te deja arquitecto: api/services/store/schemas fue tu decisión — y por eso este libro la enseña.

taskflow/
├── main.go                  # ensambla: mux + handlers
├── internal/
│   ├── httpapi/tasks.go     # transporte
│   ├── service/tasks.go     # negocio
│   ├── store/memory.go      # datos
│   └── domain/task.go       # contratos + errores de dominio
// internal/httpapi/tasks.go — el handler delgado
func (h *TaskHandler) create(w http.ResponseWriter, r *http.Request) {
    var in domain.CreateTaskInput
    if err := json.NewDecoder(r.Body).Decode(&in); err != nil {
        writeError(w, 400, "INVALID_JSON", "invalid json")
        return
    }
    t, err := h.svc.Create(in)
    if err != nil { writeDomainError(w, err); return }
    w.WriteHeader(http.StatusCreated)
    json.NewEncoder(w).Encode(domain.ToPublic(t))
}
// main.go — el ensamblador
func main() {
    store := store.NewMemory()
    svc := service.New(store)      // inyección explícita de dependencias
    h := httpapi.NewTaskHandler(svc)
    mux := http.NewServeMux()
    mux.HandleFunc("POST /tasks", h.create)
    http.ListenAndServe(":8000", mux)
}
# La estructura de packages no cambia con el router — chi solo es mejor mux
taskflow/
├── cmd/api/main.go          # entrypoint separado (convención cmd/)
└── internal/
    ├── httpapi/ · service/ · store/ · domain/

Qué te cuesta: en Go los frameworks (Gin, Echo) casi nunca imponen estructura — la comunidad convergió en cmd/ + internal/ + packages planos. El “framework de arquitectura” de Go es la convención misma, y internal/ la hace cumplir el compilador.

Refactoriza mi API de tareas (ahora en un solo archivo) a arquitectura
por capas en [Express+TypeScript / FastAPI+Python / Go]. Estructura:
routes/handlers (solo traducen HTTP), services (reglas de negocio,
errores de dominio), storage (persistencia), schemas (contratos de
entrada/salida), y un ensamblador (index/main) que solo conecta piezas.
Requisito: los handlers deben quedar "tontos" — la regla "no borrar
tarea con subtareas" vive en el servicio. Muéstrame el árbol de carpetas
y el código del handler delgado.

16.6 ¿Por qué cada stack lo hace así?

Lo único que necesitas llevarte: la estructura de capas es idéntica en los tres; lo que cambia es cómo se conectan las piezas (routers montados, Depends declarativo, o wiring explícito en main.go). El detalle:

  • Express: los routers se montan con app.use("/tasks", router) — composición por middleware.
  • FastAPI: include_router + Depends — la inyección de dependencias es declarativa (la pides en la firma y el framework la resuelve).
  • Go: la inyección es manual y explícita: service.New(store), NewTaskHandler(svc) — el wiring se ve en main.go. Más verboso, imposible de no entender.

Y un detalle Go que vale oro: internal/ no es una convención — el compilador prohíbe que otros módulos importen paquetes bajo internal/. La frontera de capas la hace cumplir la herramienta, no la disciplina.

Esta decisión es estructural en espíritu (separar transporte de negocio es innegociable) pero material en forma — hay varias cimentaciones legítimas:

Alternativa Qué es Qué te cuesta Cuándo elegirla
Capas (este libro) routes → services → storage Disciplina manual de fronteras El default pragmático — 80% de los proyectos
MVC Model-View-Controller clásico “View” no aplica bien a APIs JSON Frameworks que lo traen (Django, Rails)
Clean / Hexagonal Puertos y adaptadores, dominio al centro Muchas interfaces y carpetas para un CRUD Dominios complejos, larga vida, equipos grandes
Feature folders Todo de “tasks” junto (ruta+servicio+storage) Acopla capas dentro del feature Proyectos chicos, microservicios por dominio
Framework impone estructura NestJS modules, Django apps Aprendes “la forma del framework” Cuando el equipo ya usa ese framework

Regla de experiencia: empieza en capas; migra a hexagonal cuando el dominio lo grite — no antes. La sobre-ingeniería temprana también es una mala cimentación: pilotes para una casa de un piso.

16.7 Errores comunes

  • El handler gordo: lógica de negocio dentro de la ruta → imposible de testear sin HTTP, imposible de reusar.
  • Imports cruzados: storage importando schemas de HTTP, servicios importando express/fastapi — la flecha de dependencias siempre apunta hacia adentro (datos no conocen transporte).
  • Carpetas por tipo técnico en vez de por capa: controllers/, utils/, misc/ — donde todo acaba siendo cajón desastre.
  • Sobre-ingeniería temprana: interfaces, factories y repositorios abstractos “por si acaso” en un CRUD de memoria. Las capas que usamos bastan; más abstracción cuando duela de verdad.

16.8 Buenas prácticas

  • La regla del test: si no puedes testear la regla de negocio sin levantar el servidor, está en la capa equivocada.
  • El ensamblador es plano: main/index solo conecta piezas — leerlo debe contar la arquitectura entera.
  • Errores de dominio viven en el dominio (Cap. 6): los handlers los traducen, no los inventan.
  • Estructura que un extraño navega en 2 minutos: si alguien clona el repo y no encuentra “dónde está la regla X”, la estructura falló.

Un repositorio (repo) es la carpeta del proyecto con todo su historial Git adentro. GitHub/GitLab son servicios que hospedan repos para compartirlos y respaldarlos.

EXTRA Las taxonomías de arquitectura que listan los roadmaps backend —

  • Monolítico — todo en un proceso; el default correcto al empezar
  • Microservicios — procesos separados por dominio; paga cuando el equipo ya no cabe en un monolito
  • Serverless — funciones por evento, sin servidor visible
  • Escalado vertical (máquina más grande) vs horizontal (más máquinas — exige app sin estado local)
  • Balanceadores de carga — reparten el tráfico entre réplicas

Todos aparecen como capítulos o decisiones en este libro; la lista sirve de mapa de vocabulario para reconocerlos en una entrevista. → cap-30..35, nu-05, Ap. K.

16.9 Ejercicio

  1. Refactoriza Taskflow a la estructura por capas de tu stack — mueve reglas, no solo archivos.
  2. Agrega una segunda entidad (lists — listas de tareas) como router paralelo, sin duplicar wiring.
  3. Sube la apuesta: implementa storage detrás de una interfaz/contrato mínimo para que “lista en memoria” sea intercambiable.

16.10 Mini reto

Mueve una regla de negocio del handler al servicio (ej: “no puede haber dos tareas con el mismo título en la misma lista”) y escribe un párrafo respondiendo: ¿qué se ganó? ¿Qué se volvió posible que antes no lo era? (Pista: piensa en el Cap. 28 — tests sin HTTP.)

Ejercicio 1. Tras el refactor, main/index solo ensambla (router + middleware + handlers de error), services/ tiene las reglas, storage/ la lista, schemas/ los contratos — el árbol de la sección Implementación.

Ejercicio 2. lists es otro router con su propio par servicio/storage: include_router(lists_router) / app.use("/lists", …) / mux.HandleFunc("GET /lists", …) — la estructura escala por dominio sin tocar lo existente.

Ejercicio 3. El contrato mínimo de storage: métodos list/create/get/ update/delete (o Store interface en Go). Cambiar lista→Postgres en el Cap. 10 = cambiar solo la implementación, cero handlers.

Mini reto. Lo ganado: la regla se puede (a) testear sin HTTP (svc.create_task duplicado → ConflictError), (b) reutilizar desde otro endpoint o un futuro WebSocket/importador, (c) leer en un solo lugar — la fuente de verdad del negocio. Lo que se volvió posible: tests de servicio rápidos y un handler que solo traduce.

16.11 Vocabulario técnico del capítulo

Una variable es una caja con nombre donde guardas un valor: let total = 42 guarda el 42 bajo el nombre total. Puedes leerla y cambiarla después (total = 50). const = caja que no se puede reemplazar.

Un objeto es una colección de datos con nombre: {name: "Ana", age: 30} — cada dato es una propiedad (clave → valor). Python los llama dict, Go los arma con struct, TS con objetos/interface.

Un bucle repite una acción por cada elemento o hasta cumplir una condición: for task in tasks hace algo con cada tarea. map y filter son bucles disfrazados de funciones — transforman/filtran colecciones sin for explícito.

Una clase es el molde de un objeto: define qué datos y qué métodos tiene. class Task es el molde; new Task() es una instancia concreta. Go no tiene clases — usa struct + métodos sueltos.

null (TS), None (Py), nil (Go) = “aquí no hay valor”. Es la respuesta a “¿qué devuelvo cuando no hay nada?” — y la fuente del bug más famoso de la historia (su inventor lo llamó “el error del billón de dólares”). Por eso el código revisa if x is not None.

return hace dos cosas a la vez: devuelve el resultado Y termina la función — lo que esté debajo nunca corre. return task = “aquí está el plato, salgo de la cocina”.

Un método es una función que vive dentro de un objeto/clase: task.save() — save es un método de task. La diferencia con una función suelta: el método conoce al objeto que lo contiene (this/ self).

Una firma (HMAC) prueba que un mensaje es auténtico y no fue tocado: se calcula con un secreto sobre el contenido. Los webhooks la usan para que verifiques “esto realmente vino de Stripe”.

Un framework es un esqueleto de aplicación ya decidido: te da la estructura (rutas, validación, errores) y tú llenas la lógica. Diferencia con librería: la librería la llamas tú; el framework te llama a ti.

El build (construcción) es el proceso que convierte tu código fuente en algo ejecutable/entregable: compilar TypeScript a JS, empaquetar el frontend, armar la imagen Docker. Lo que se despliega es el resultado del build, no tu código crudo.

Una base de datos es el programa que guarda datos de forma permanente y los responde rápido. Relacional (Postgres): tablas con relaciones. No relacional: documentos, clave-valor, grafos — cada una para una forma de dato distinta.

Una query es la pregunta que le haces a la base de datos en SQL: SELECT * FROM tasks WHERE done = false. La BD traduce la pregunta a un plan de búsqueda — por eso los índices importan.

Un ORM (Object-Relational Mapper) traduce entre objetos del código y filas de la tabla: task.save() en vez de INSERT. Cómodo para el CRUD, peligroso si no sabes qué SQL genera (el N+1 nace ahí).

16.12 Lo que deberías saber hacer ahora