6.5 En forma correcta
Esta traducción fue asistida por IA y revisada superficialmente. Si encuentras errores o algo no suena natural, escríbeme — lo agradezco.
Una petición verbal y una escrita son peticiones distintas. La primera se olvida, se distorsiona, se cuenta mal. La segunda tiene forma: campos, tipos, orden. El receptor sabe exactamente qué se le pasó - no porque lo adivinara, sino porque la forma no deja margen para el error. El servidor sigue respondiendo con cadenas. La forma aún no está definida.
Lo que necesitas
POST /tasks acepta un cuerpo en la petición - el título de la tarea. Ahora mismo el
handler no puede verlo. Los cuatro handlers devuelven cadenas, cuando deberían devolver
estructuras.
Json<T> funciona en ambas direcciones: como argumento - axum lee el cuerpo de la petición
y lo deserializa en el tipo T que sea necesario; como tipo de retorno - axum serializa T
en JSON y añade la cabecera de respuesta correcta.
La construcción
crates/api aún no depende de tq-core ni de serde. Añade las dependencias a
crates/api/Cargo.toml:
# crates/api/Cargo.toml - CAMBIADO
[dependencies]
axum = "0.8"
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
tq-core = { path = "../core" }
Ahora actualiza crates/api/src/routes.rs. Se necesita un tipo para el cuerpo de la
petición - AddRequest con un campo title. Vive en el mismo archivo, junto al handler
que lo usa:
// crates/api/src/routes.rs - CAMBIADO
use axum::{
Json, Router,
extract::Path,
routing::{get, patch, post},
};
use serde::Deserialize;
use tq_core::task::Task;
#[derive(Deserialize)]
struct AddRequest {
title: String,
}
async fn add(Json(req): Json<AddRequest>) -> Json<Task> {
Json(Task::new(1, &req.title).unwrap())
}
async fn list() -> Json<Vec<Task>> {
Json(vec![])
}
async fn get_task(Path(id): Path<u64>) -> Json<Task> {
Json(Task::new(id, "stub").unwrap())
}
async fn done(Path(id): Path<u64>) -> Json<Task> {
let mut task = Task::new(id, "stub").unwrap();
task.complete();
Json(task)
}
pub fn router() -> Router {
Router::new()
.route("/tasks", post(add).get(list))
.route("/tasks/{id}", get(get_task))
.route("/tasks/{id}/done", patch(done))
}
Json(req): Json<AddRequest> - el mismo destructuring que Path(id): Path<u64>: el
wrapper se desenvuelve en el argumento.
Task::new construye una tarea a partir de un id y un título. Los handlers lo devuelven
con valores stub - desaparecerán en cuanto se añada almacenamiento real. .unwrap() es
aceptable mientras no haya manejo de errores. La forma de la respuesta ya es correcta.
El resultado
$ make serve
En otro terminal:
$ curl -X POST localhost:3000/tasks \
-H "Content-Type: application/json" \
-d '{"title":"buy milk"}'
{"id":1,"title":"buy milk","status":"Todo","created_at":"2025-06-09T10:00:00Z"}
$ curl localhost:3000/tasks
[]
$ curl localhost:3000/tasks/42
{"id":42,"title":"stub","status":"Todo","created_at":"2025-06-09T10:00:00Z"}
$ curl -X PATCH localhost:3000/tasks/7/done
{"id":7,"title":"stub","status":"Done","created_at":"2025-06-09T10:00:00Z"}
Cuatro handlers responden con estructuras correctas. Los datos son stubs - todavía no hay almacén. Pero la forma está definida: el cliente sabe qué va a recibir.
make ci pasa: los mismos 38 tests.
El código completo de
tqpara este capítulo está en6-the-port/05-in-proper-form/.
Lore: Content-Type
-H "Content-Type: application/json" es una cabecera obligatoria al enviar un cuerpo JSON.
axum la lee y sabe cómo parsear el cuerpo. Sin ella, axum devuelve 415 Unsupported Media Type sin llamar al handler.
En la respuesta, axum establece Content-Type: application/json por sí mismo. Las
peticiones GET no llevan cuerpo, y Content-Type describe exactamente eso - el cuerpo.
Sin cuerpo, la cabecera no es necesaria.