Saltar al contenido
Framework Ecosystems LabsUn contrato, muchos ecosistemas, la misma prueba.

Clase 005 — Idiomático frente a traducido#

⬅️ 004 · 📚 Parte 0 · 🎓 Clases · 006 ➡️ Parte 0 — El método: qué es un framework y cómo se compara · Nivel 🟢 introductorio · Pista backend (Backend y API) ✅ Clase construida — 4 implementaciones verificadas contra contrato.json.

🎯 Objetivo#

Reconocer cuándo un código es el de otro framework disfrazado: escrito en la sintaxis correcta, con las importaciones correctas, y con las suposiciones de otro sitio.

Es el error más caro de los que comete alguien que cambia de ecosistema, porque no da la cara al escribirlo. Compila, pasa las pruebas y llega a producción.

📚 Resultados de aprendizaje#

Al terminar podrás:

🧩 La situación#

Alguien que lleva cinco años con Express llega a un proyecto de FastAPI. Escribe su primera ruta y la escribe así:

async def crear(peticion: Request):
    datos = await peticion.json()
    titulo = datos.get("titulo")
    if not titulo:
        return JSONResponse({"code": "TITULO_INVALIDO"}, status_code=422)

Funciona. Es Python válido, es FastAPI válido, y hace exactamente lo que tiene que hacer. En la revisión nadie dice nada, porque no hay nada que señalar: no hay un error, hay una ausencia.

Esta clase pone esa ruta al lado de su versión idiomática y las somete al mismo contrato. Las dos pasan el camino feliz. Y a partir del segundo caso dejan de parecerse.

🧮 El contrato#

Cada implementación expone la misma ruta dos veces:

Las dos escriben en la misma lista, porque son el mismo servicio.

# Petición Idiomática Traducida
1-2 {"titulo":"comprar pan"} 201 201idénticas
3-4 {"titulo":""} 422 422siguen idénticas
5-6 {"titulo":" "} 422 201 ← aquí se rompe
7 GET /tareas/3 titulo son cinco espacios
8-9 {"titulo":" con espacios "} 201, guarda "con espacios" 201, guarda " con espacios "
10 GET /tareas total: 5 — la misma lista
11 GET /comparacion medido, no declarado

Los cuatro primeros casos son la clase entera. Si el contrato solo tuviera esos, las dos rutas serían indistinguibles y la traducción parecería correcta.

Y el caso 11 no se cree nada: /comparacion ejecuta las dos versiones con el mismo cuerpo válido y compara los resultados en lugar de afirmar que coinciden. Escribir «hacen lo mismo» sin comprobarlo sería el mismo error que la clase denuncia.

<!-- generado: fichas -->

📖 Las palabras que esta clase define#

Si alguna de estas no te dice nada todavía, esta es la clase donde se aprende. Las definiciones viven en el glosario, que reúne las del programa entero.

Palabra Qué significa
Convención (Convención sobre configuración) Una regla que el framework da por supuesta sin que la escribas: que la clase RaizController atiende la ruta raiz#mostrar, que las plantillas están en un directorio concreto. Menos código, y más cosas que aprender antes de poder leerlo.
Idiomático Código escrito como lo escribiría quien conoce ese framework, en lugar de traducido literalmente desde otro. Un Express escrito como si fuera Spring funciona y pierde todo lo que hacía valioso a Express.

🧰 Las piezas de esta clase, una por una#

Antes del código: qué es cada framework, qué versión se está usando y qué hace falta para ejecutarlo. Todo lo de esta sección sale de los archivos reales del repositorio —el catálogo, la receta de arranque y el manifiesto de dependencias de cada ecosistema—, así que no puede quedarse desactualizado sin que la validación lo detecte.

Framework Qué es Desde Licencia Quién lo mantiene
Express framework web de Node.js (JavaScript) 2010 MIT OpenJS Foundation
FastAPI framework web de Python (Python) 2018 MIT proyecto independiente
Spring Boot framework de aplicación de JVM (Java) 2014 Apache-2.0 Broadcom/VMware y colaboradores
Ruby on Rails full-stack-framework de Ruby (Ruby) 2004 MIT proyecto independiente

🔧 Express#

Definió el modelo de middleware encadenado que copiaron casi todos los frameworks de Node.js. Minimalista no significa biblioteca: posee el bucle de peticiones.

Preparar sus dependencias, dentro de su directorio:

pnpm install --silent --ignore-scripts

Arrancarla suelta, sin el verificador:

PORT=3000 node server.mjs

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
package.json manifiesto de Node.js: nombre, tipo de módulo y dependencias con su rango de versión
pnpm-lock.yaml archivo de bloqueo: la versión exacta de cada dependencia y de sus dependencias
pnpm-workspace.yaml raíz de instalación propia, y la prohibición de ejecutar scripts al instalar
server.mjs código JavaScript (módulo ES)

🔧 FastAPI#

Deriva validación, serialización y documentación OpenAPI de las anotaciones de tipo. Demostró que el tipado opcional de Python podía ser infraestructura, no adorno.

Arrancarla suelta, sin el verificador:

PORT=3000 python -m uvicorn main:app --host 127.0.0.1 --port 3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
main.py código Python
requirements.txt dependencias de Python, una por línea, con versión fijada

🔧 Spring Boot#

Autoconfiguración y servidor incrustado sobre Spring. Convirtió un framework famoso por su configuración XML en uno de arranque inmediato.

Preparar sus dependencias, dentro de su directorio:

mvn -q -B package -DskipTests

Arrancarla suelta, sin el verificador:

PORT=3000 java -jar target/clase-005-1.0.0.jar --server.port=3000

Qué hay dentro de su directorio:

Archivo Qué es
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca
pom.xml manifiesto de Maven: el proyecto, su Java, sus dependencias y cómo se empaqueta
src/main/java/labs/Aplicacion.java código Java

🔧 Ruby on Rails#

Origen de «convención sobre configuración» y de las migraciones de base de datos tal como se entienden hoy. Casi todos los frameworks completos posteriores citan su influencia.

Preparar sus dependencias, dentro de su directorio:

bundle install --quiet

Arrancarla suelta, sin el verificador:

PORT=3000 bundle exec puma -b tcp://127.0.0.1:3000 config.ru

Qué hay dentro de su directorio:

Archivo Qué es
.bundle/config archivo del proyecto
Gemfile dependencias de Ruby
config.ru punto de entrada de Rack, el estándar de servidores de Ruby
ejecutar.json la receta que usa el verificador: qué hace falta, cómo se prepara y cómo arranca

Si alguna cadena de herramientas no está en tu máquina, node scripts/doctor.mjs dice cuál falta y con qué comando se instala. No hace falta tenerlas todas: el verificador ejecuta lo que encuentra y declara lo que omitió.

<!-- fin generado: fichas -->

🌐 Las implementaciones — el código a la vista#

Cuatro frameworks, cuatro traducciones distintas. En tres de ellos lo que se pierde lo daba el framework; en el cuarto no lo daba nadie, y por eso la traducción es igual de peligrosa.

Express · express/server.mjs — traducido desde Rails#

La idiomática:

function tituloValido(cuerpo) {
  const titulo = cuerpo?.titulo;
  if (typeof titulo !== "string") return null;
  const limpio = titulo.trim();
  return limpio.length === 0 ? null : limpio;
}

En Express la validación se escribe. Componer con piezas pequeñas es el modelo del ecosistema, no una carencia [casciaro-node-patterns]. No hay atajo, y por eso está en una función con nombre en lugar de repartida por el manejador: es la única forma de que dos rutas apliquen exactamente la misma regla.

Fíjate en que devuelve el título ya normalizado, no un sí o un no. Validar y normalizar en el mismo sitio evita que una ruta recorte y otra no.

La traducida:

app.post("/traducido/tareas", (peticion, respuesta) => {
  const titulo = peticion.body?.titulo;
  if (!titulo) {
    respuesta.status(422).json({ code: "TITULO_INVALIDO" });
    return;
  }

if (!titulo) es la comprobación de presencia tal y como suena: ¿vino algo? Y es verdad que cubre el campo ausente y la cadena vacía.

Lo que no cubre —y en Rails sí cubría— es " ". Cinco espacios son un texto perfectamente presente para JavaScript.

 * Lo que no cubre —y en Rails sí cubría— es `"     "`. Cinco espacios son un
 * texto perfectamente presente para JavaScript. El traductor no omitió la
 * validación: la tradujo mal, que es mucho más difícil de ver.

Este caso es el más incómodo de los cuatro, y por eso va primero: aquí el framework no da nada. Express no valida ni en la versión idiomática ni en la traducida. Lo que se traduce mal no es una pieza del framework: es una regla que alguien creía que venía puesta.

De ahí la forma general del error: traducir no es traducir sintaxis, es arrastrar suposiciones sobre quién se encarga de qué.

FastAPI · fastapi/main.py — traducido desde Express#

La idiomática — y «los tipos hacen el trabajo» es literalmente el primer argumento de venta del framework [fastapi-features]:

class Tarea(BaseModel):
    """Las reglas viven en el TIPO. No hay `if` que se pueda olvidar."""

    titulo: str = Field(min_length=1)

    @field_validator("titulo")
    @classmethod
    def sin_espacios_sobrantes(cls, valor: str) -> str:
        limpio = valor.strip()
        if not limpio:
            raise ValueError("titulo no puede estar vacio")
        return limpio
@app.post("/idiomatico/tareas", status_code=201)
def crear_idiomatico(tarea: Tarea) -> JSONResponse:
    """El manejador NO valida: recibe un objeto que ya cumple."""

La traducida:

    datos = await peticion.json()
    titulo = datos.get("titulo")
    if not titulo:
        return JSONResponse({"code": "TITULO_INVALIDO"}, status_code=422)

await peticion.json() es el equivalente exacto de req.body. Y aquí la pérdida es mucho mayor que en Express, porque lo que se deja de usar sí existía:

    Y al escribir `Request` en lugar de `Tarea` en la firma, FastAPI deja de
    saber que espera esta ruta: no valida, no convierte y no documenta. La
    diferencia entre las dos no es de estilo — es de cuanto framework se esta
    usando.

Y esa última pérdida —la documentación— es la que no se ve leyendo el código, así que la clase la mide:

    esquema = app.openapi()["paths"]
    def tiene_cuerpo(ruta: str) -> bool:
        return "requestBody" in esquema[ruta]["post"]

Ejecuta GET /comparacion y verás cuerpo_documentado_en_la_idiomatica: true frente a cuerpo_documentado_en_la_traducida: false. La ruta traducida aparece en el esquema OpenAPI generado sin ningún cuerpo declarado: quien consulte la documentación de esa API no sabrá qué mandarle.

Cambiar un tipo por Request en la firma apagó una funcionalidad entera del framework, y no hubo aviso.

Spring Boot · spring-boot/…/Aplicacion.java — traducido desde Express#

La idiomática:

    public record Tarea(@NotBlank String titulo) {
    }
    @PostMapping("/idiomatico/tareas")
    public ResponseEntity<Map<String, Object>> crearIdiomatico(@Valid @RequestBody Tarea tarea) {
        return ResponseEntity.status(201).body(guardar(tarea.titulo().trim()));
    }

@NotBlank es de Jakarta Bean Validation —un estándar, no de Spring [walls-spring-in-action]— y significa «no nulo y no vacío tras recortar». Esa última parte es justo la que la traducción pierde.

La traducida:

    public ResponseEntity<Map<String, Object>> crearTraducido(
            @RequestBody(required = false) Map<String, Object> cuerpo) {
        Object titulo = cuerpo == null ? null : cuerpo.get("titulo");
        if (titulo == null || titulo.toString().isEmpty()) {
            return ResponseEntity.status(422).body(Map.of("code", "TITULO_INVALIDO"));
        }

Map<String, Object> es el equivalente exacto de req.body. Y en un lenguaje tipado eso cuesta más que en ninguno:

     * Lo que se pierde no es solo `"     "`: al declarar un mapa en lugar de un
     * tipo, el controlador deja de tener contrato. Nadie que lea la firma sabe
     * que espera esta ruta, ninguna herramienta puede documentarla, y el
     * compilador no puede ayudar. En un lenguaje tipado, renunciar al tipo es
     * renunciar a casi todo.

Es la misma pérdida que en FastAPI, en un ecosistema donde además duele el doble: el motivo de elegir Java es que el compilador vigile, y un Map<String, Object> lo deja mirando.

Rails · rails/config.ru — traducido desde Express#

La idiomática:

class Tarea
  include ActiveModel::Model

  attr_accessor :titulo

  validates :titulo, presence: true

  def normalizado
    titulo.to_s.strip
  end
end
  def idiomatico
    tarea = Tarea.new(titulo: params[:titulo])
    return render(json: { code: "TITULO_INVALIDO" }, status: 422) unless tarea.valid?

    render json: guardar(tarea.normalizado), status: 201
  end

presence: true usa blank?, y en Rails un texto de solo espacios es blank. Es la convención sobre la configuración de la que Rails hizo bandera [rails-doctrine]: la regla habitual viene puesta, y quien quiera otra la escribe. Por eso Rails es el framework del que se traduce en el ejemplo de Express: es el que trae esa regla puesta.

La traducida:

  def traducido
    cuerpo = JSON.parse(request.body.read) rescue {}
    titulo = cuerpo["titulo"]
    if titulo.nil? || titulo == ""
      return render(json: { code: "TITULO_INVALIDO" }, status: 422)
    end

Y aquí hay una segunda pérdida, específica de Rails, que merece leerse entera:

  # Y de paso se salta los parámetros fuertes de Rails, que es la pieza que
  # decide qué campos del cuerpo pueden llegar al modelo. Aquí no hay modelo, así
  # que tampoco hay quien lo eche de menos — hasta que alguien manda un campo de
  # más.

Leer el cuerpo crudo esquiva params, y con él los parámetros fuertes — la pieza que Rails añadió en 2012 precisamente porque el problema contrario había causado incidentes reales de asignación masiva. La traducción no solo pierde una validación: desactiva una defensa.

🔬 Comparación#

Framework Quién valida en la idiomática Qué pierde la traducida
Express una función escrita a mano la regla completa: nadie la ponía
FastAPI Pydantic, antes de entrar al manejador validación, tipos y documentación generada
Spring Boot Jakarta Bean Validation, antes del método validación, tipos y ayuda del compilador
Rails ActiveModel con presence: true validación y los parámetros fuertes

Tres cosas que se leen de la tabla:

⚠️ Errores frecuentes#

✅ Verificación#

node scripts/run-class.mjs 005

Cuatro implementaciones, once casos cada una. El verificador declara las que omitió por no encontrar su cadena de herramientas; node scripts/doctor.mjs dice cuáles faltan y cómo se instalan.

Para ver la comparación medida de una de ellas, con el servidor levantado:

curl -s http://127.0.0.1:4100/comparacion

🧪 Reto de transferencia#

  1. Escribe la tercera versión de la ruta en el framework que uses a diario, traduciéndola desde el que usabas antes. Pásale el contrato de esta clase y mira en qué caso se cae.
  2. Busca en tu propio código una firma que reciba el cuerpo crudo — Request, Map<String, Object>, req.body, params sin filtrar — y pregúntate qué pieza del framework se apagó al escribirla así.
  3. Añade un caso al contrato con un título de 500 caracteres. Ninguna de las ocho rutas lo rechaza hoy. Decide dónde pondrías esa regla en cada framework: esa decisión es la clase 039 entera.

🔗 Enlaces#

Fuentes#