Parte 11 — Proyecto integrador políglota · ⏱️ Duración estimada: 90 min · Nivel: Intermedio ✅ Clase construida — 10 implementaciones del núcleo verificadas contra
casos.json.
Construimos el corazón del sistema: el componente de API o servicio backend. Es donde vive la lógica de
negocio; recibe una petición y devuelve una respuesta compuesta por dos cosas inseparables: un código
de estado (¿fue bien?) y unos datos (el resultado). Hoy implementamos el caso feliz: responder 200
(OK) con el dato recibido.
Que la respuesta lleve siempre un código de estado no es un detalle de HTTP: es un principio de diseño de
sistemas distribuidos. Nygard, en Release It!, dedica su libro a que un servicio no basta con que
funcione en la demo —tiene que comportarse bien cuando algo falla—, y el primer eslabón de ese buen
comportamiento es comunicar con honestidad el desenlace de cada petición. Un 200 que en realidad
encubre un error es, en sus palabras, una bomba de tiempo: el cliente cree que todo fue bien y sigue
adelante sobre datos rotos. Newman, en Building Microservices, complementa: la API es el contrato público
del servicio, y el código de estado es la parte del contrato que dice cómo interpretar el resto.
Al finalizar, podrás:
| # | Tema | Por qué importa |
|---|---|---|
| 1 | Servicio/API | Atiende peticiones y contiene la lógica |
| 2 | Código de estado | 200 OK, 404, 500: comunica el desenlace |
| 3 | Respuesta | Estado + datos: lo que el cliente interpreta |
El componente de API es el servicio que atiende peticiones y devuelve respuestas; concentra la lógica
del sistema y, por eso, es su cerebro. El código de estado es el número que resume el resultado: 200
OK, 404 no encontrado, 500 error del servidor. La respuesta es la suma de ese estado y los datos;
es literalmente lo que el cliente va a consumir e interpretar.
La familia de códigos (2xx éxito, 4xx culpa del cliente, 5xx culpa del servidor) es un lenguaje común que
permite que un frontend en TypeScript, un cliente móvil en Kotlin y un curl en un script entiendan lo
mismo sin ponerse de acuerdo caso por caso. Ese es el poder de un contrato bien diseñado: reduce la
coordinación. Nygard insiste en que las respuestas de error son tan parte de la API como las de éxito —un
servicio que solo documenta el 200 es un servicio a medio diseñar—, porque los patrones de estabilidad
(timeouts, reintentos, circuit breakers) se apoyan justamente en distinguir un 503 transitorio de un
400 definitivo.
El frontend pide un dato; el backend responde 200 con el dato, o 404 si no existe, o 500 si algo se
rompió por dentro. Ese pequeño número gobierna lo que el cliente hará después: mostrar el resultado,
enseñar "no encontrado" o reintentar más tarde. El componente de API es el lugar donde a menudo se elige
Go, Java o C# por su rendimiento y su ecosistema de servidores (Spring, ASP.NET, Gin), o Python con FastAPI
por la velocidad de desarrollo. Empezar por el caso 200 fija la forma de la respuesta —estado más datos—
que los casos de error rellenarán después.
n (el dato solicitado)respuesta=200 datos=<n>Especificación y verificación en casos.json:
| stdin | esperado |
|---|---|
5 |
respuesta=200 datos=5 |
0 |
respuesta=200 datos=0 |
42 |
respuesta=200 datos=42 |
LEER n ; ESCRIBIR estado 200 y datos=n
Mismo algoritmo, forma idiomática en cada lenguaje. Todas producen la salida de casos.json.
Cada bloque es el archivo real de implementaciones/: el enlace de cada lenguaje abre su fuente, y el comando de al lado lo ejecuta.
python/main.py · python main.pyimport sys
n = int(sys.stdin.readline())
print(f"respuesta=200 datos={n}")
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
javascript/main.mjs · node main.mjsimport { readFileSync } from "node:fs";
const n = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`respuesta=200 datos=${n}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
typescript/main.ts · pnpm exec tsx main.tsimport { readFileSync } from "node:fs";
const n: number = parseInt(readFileSync(0, "utf8").trim(), 10);
console.log(`respuesta=200 datos=${n}`);
🧬 El mismo programa en la familia JavaScript / web: Dart · ActionScript
java/Main.java · java Main.javaimport java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
public class Main {
public static void main(String[] args) throws IOException {
BufferedReader br = new BufferedReader(new InputStreamReader(System.in));
int n = Integer.parseInt(br.readLine().trim());
System.out.println("respuesta=200 datos=" + n);
}
}
🧬 El mismo programa en la familia JVM: Kotlin · Scala · Groovy · Clojure
csharp/Program.cs · dotnet runusing System;
int n = int.Parse(Console.In.ReadToEnd().Trim());
Console.WriteLine($"respuesta=200 datos={n}");
🧬 El mismo programa en la familia .NET: F# · VB.NET
go/main.go · go run main.gopackage main
import (
"bufio"
"fmt"
"os"
"strconv"
"strings"
)
func main() {
line, _ := bufio.NewReader(os.Stdin).ReadString('\n')
n, _ := strconv.Atoi(strings.TrimSpace(line))
fmt.Printf("respuesta=200 datos=%d\n", n)
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
rust/main.rs · rustc main.rs -o main && ./mainuse std::io::Read;
fn main() {
let mut s = String::new();
std::io::stdin().read_to_string(&mut s).unwrap();
let n: i64 = s.trim().parse().unwrap();
println!("respuesta=200 datos={n}");
}
🧬 El mismo programa en la familia Sistemas: Zig · Nim · D
c/main.c · cc main.c -o main && ./main#include <stdio.h>
int main(void) {
long n;
if (scanf("%ld", &n) != 1) return 1;
printf("respuesta=200 datos=%ld\n", n);
return 0;
}
🧬 El mismo programa en la familia C / llaves: C++ · Objective-C
sql/main.sql · sqlite3 :memory: < main.sql-- SQL devuelve filas; aqui, la respuesta simulada.
WITH t(n) AS (VALUES (5))
SELECT printf('respuesta=200 datos=%d', n) AS resultado FROM t;
🧬 El mismo programa en la familia Lógica y declarativa: Prolog · Datalog
php/main.php · php main.php<?php
$n = (int) trim(fgets(STDIN));
echo "respuesta=200 datos=$n\n";
🧬 El mismo programa en la familia Scripting dinámico: Ruby · Perl · Lua · Tcl · R
SQL es declarativo: no lee de stdin como los demás; su implementación muestra la misma idea sobre una tabla de casos, y el verificador la marca como ilustrativa.
El contrato (casos.json) es deliberadamente simple para que el foco esté en la forma de
la respuesta: de 5 sale respuesta=200 datos=5; de 0, respuesta=200 datos=0. Un backend real leería
JSON de una petición HTTP; aquí el "cuerpo de la petición" es el entero por stdin, y la respuesta es la
línea de salida. La abstracción se sostiene: leer una entrada, devolver estado + datos.
Python convierte y responde en dos gestos: n = int(sys.stdin.readline()) parsea el dato pedido, y la
f-string f"respuesta=200 datos={n}" arma la respuesta con su código fijo. La conversión a int importa:
un backend valida y tipa lo que entra antes de confiar en ello.
Rust hace explícito ese contrato de tipos donde Python lo deja implícito:
let n: i64 = s.trim().parse().unwrap();
println!("respuesta=200 datos={n}");
La anotación : i64 y el parse() dicen "espero un entero de 64 bits, y si no lo es, esto no continúa".
El unwrap() es la versión abrupta de manejar el error —en producción sería un match que devolvería un
400—, pero el mensaje de Klabnik y Nichols en The Rust Programming Language ya está: los errores son
valores que el tipo te obliga a mirar. Un backend en Rust no puede "olvidar" que el parseo puede fallar.
Go parsea con strconv.Atoi devolviendo (valor, error) —el segundo, aquí ignorado con _, sería en
un servidor real la señal para responder 400—. C usa scanf("%ld", &n) y comprueba que leyó
exactamente un valor (!= 1) antes de responder, saliendo con return 1 si no. SQL ilustra la
respuesta como una fila calculada, recordándonos que su modelo es "devolver conjuntos", no "devolver
códigos HTTP". En los cinco, el patrón es el mismo: validar la entrada, componer estado + datos. La
diferencia está en cuánto te obliga el lenguaje a tomar en serio el fallo del parseo, que es exactamente el
fallo que un backend descuidado convierte en un 500.
La respuesta de una API es "estado + datos", y la parte interesante es qué pasa cuando la entrada no es lo esperado.
| Clase de diferencia | Observación entre lenguajes |
|---|---|
| Sintáctica | int(...) (Python), parse() (Rust), Atoi (Go), int.Parse (C#), scanf (C): la misma validación de tipo. |
| Semántica | Rust y Go hacen del error de parseo un valor explícito; Python lanza excepción; C devuelve un código. Ese error es, en un backend real, la diferencia entre un 400 y un 500. |
| Paradigmática | Los servicios imperativos componen y emiten una respuesta; SQL devuelve filas de un conjunto, no códigos de estado. |
La lección de Nygard aplica letra por letra: el caso feliz es el 20 % del diseño; el 80 % es qué respondes cuando la entrada es basura, el vecino no contesta o la base de datos tarda. El código de estado es la herramienta con la que ese 80 % se hace comunicable.
Express (JS), Spring (Java), ASP.NET (C#), Gin (Go) y FastAPI (Python) construyen APIs sobre este mismo esqueleto: recibir una petición, ejecutar la lógica, devolver estado + datos. Cambian el rendimiento, el sistema de tipos y la ergonomía, pero el contrato —código de estado más cuerpo— es idéntico, y por eso saltar de un framework a otro es aprender la forma, no el fondo.
Los mismos casos para todas las implementaciones: casos.json. Verifica la equivalencia:
python scripts/verificar_equivalencia.py 168
Detalle en reto.md.
500 → solución: parsear y validar en la frontera y responder 400 cuando la petición sea inválida.404. 200 es OK, 400 es petición inválida del cliente, 500 es error interno del servidor y 503 es "no disponible ahora, reintenta".Libros de la parte:
Libros de los lenguajes del núcleo:
⏮️ Clase 167 · 📂 Parte · 📚 Índice · 🌐 Atlas · Clase 169 ⏭️