Proyecto: API web con Axum en Rust
En este proyecto vas a construir un servidor web de verdad: una pequeña API de tareas (to-do). Otros programas (una página web, una app del móvil o simplemente curl en tu terminal) le envían peticiones HTTP, y ella responde con JSON. Es lo mismo que hace funcionar el “back end” de la mayoría de las apps que usas.
Usaremos axum, un framework web popular y amigable creado por el equipo de Tokio. Funciona sobre Rust asíncrono y serde, así que este proyecto junta mucho de lo que ya has aprendido: structs, Result, smart pointers (punteros inteligentes) y estado compartido entre hilos. Si hiciste el proyecto de la lista de tareas en la terminal, reconocerás la idea. Esta vez la lista de tareas vive detrás de una dirección web.
Lo que vas a construir
Sección titulada «Lo que vas a construir»Cuando termines, tu servidor responderá a estas peticiones:
| Método y ruta | Qué hace | Estado si todo va bien |
|---|---|---|
GET /todos |
Lista todas las tareas | 200 OK |
GET /todos/{id} |
Devuelve una tarea | 200 OK, o 404 Not Found |
POST /todos |
Crea una tarea a partir de JSON | 201 Created |
DELETE /todos/{id} |
Borra una tarea | 204 No Content, o 404 Not Found |
Una tarea se ve así en JSON:
{"id":1,"title":"Comprar leche","done":false}Una nota importante: un servidor web espera peticiones para siempre y necesita un puerto de red, así que no puede ejecutarse en el Playground de Rust. El código del servidor de esta página está pensado para tu propio ordenador, con cargo run. Cada paso de abajo se compiló y se ejecutó, y las respuestas que se muestran son reales. Donde una parte del programa sí puede ejecutarse en el Playground, encontrarás un botón para probarla.
Preparar el proyecto
Sección titulada «Preparar el proyecto»Crea un proyecto nuevo y añade cuatro crates:
cargo new todo-apicd todo-apicargo add serde --features derivecargo add serde_jsonAhora tu Cargo.toml tiene algo así (puede que los números de versión exactos sean más nuevos):
[dependencies]axum = "0.8"serde = { version = "1", features = ["derive"] }serde_json = "1"tokio = { version = "1", features = ["full"] }Qué hace cada uno:
- axum se encarga de HTTP: lee las peticiones y envía las respuestas.
- tokio es el runtime asíncrono que lo ejecuta todo. Consulta Async/Await.
- serde y serde_json convierten tus structs en JSON y al revés. Consulta JSON con Serde.
También necesitarás curl para enviar peticiones. Viene incluido en macOS, Linux y Windows 10 o posterior. (En Windows, escribe curl.exe en PowerShell, porque allí curl a secas significa otra cosa.)
Paso 1: ¡Hola, mundo!
Sección titulada «Paso 1: ¡Hola, mundo!»Sustituye src/main.rs por esto:
use axum::{routing::get, Router};
#[tokio::main]async fn main() { let app = Router::new().route("/", get(hello));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Escuchando en http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "¡Hola, mundo!"}Aquí trabajan juntas tres piezas:
- Un handler (manejador) es una
async fnque responde a una petición.hellodevuelve un texto, y axum lo envía como respuesta. - El router (enrutador) conecta rutas con handlers.
.route("/", get(hello))significa “cuando alguien envíe una peticiónGETa/, llama ahello”. - El listener abre el puerto 3000 en tu propio ordenador (
127.0.0.1), yaxum::serveempieza a responder peticiones. Sigue funcionando hasta que lo detengas.
Ejecútalo con cargo run. El programa imprime una línea y se queda esperando:
Escuchando en http://127.0.0.1:3000Déjalo funcionando, abre una segunda terminal y envíale una petición:
curl http://127.0.0.1:3000/¡Hola, mundo!El servidor no añade un salto de línea después del texto, así que puede que el prompt de tu terminal aparezca justo detrás de ¡Hola, mundo!. Es normal. En esta página, cada respuesta se muestra en su propia línea. También puedes abrir http://127.0.0.1:3000/ en tu navegador. Pulsa Ctrl + C en la primera terminal para detener el servidor. Tienes que detenerlo y volver a arrancarlo cada vez que cambies el código.
Paso 2: Devolver JSON a partir de un struct
Sección titulada «Paso 2: Devolver JSON a partir de un struct»Las APIs suelen responder con JSON. En axum, envuelves en Json(...) cualquier valor que implemente el Serialize de serde, y axum lo convierte en JSON y pone la cabecera content-type correcta:
use axum::{routing::get, Json, Router};use serde::Serialize;
#[derive(Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[tokio::main]async fn main() { let app = Router::new() .route("/", get(hello)) .route("/example", get(example_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Escuchando en http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello() -> &'static str { "¡Hola, mundo!"}
async fn example_todo() -> Json<Todo> { let todo = Todo { id: 1, title: String::from("Aprender axum"), done: false, }; Json(todo)}La opción -i le dice a curl que muestre también la línea de estado y las cabeceras:
curl -i http://127.0.0.1:3000/exampleHTTP/1.1 200 OKcontent-type: application/jsoncontent-length: 45
{"id":1,"title":"Aprender axum","done":false}(curl también imprime una cabecera date, que en esta página omitimos.) Las claves del JSON son exactamente los nombres de los campos del struct. Puedes comprobar esa forma del JSON en el Playground, sin servidor, porque es serde normal y corriente. Aquí también aparece el struct NewTodo que usaremos en el paso 4 para los datos que llegan:
use serde::{Deserialize, Serialize};
#[derive(Clone, Debug, Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[derive(Debug, Deserialize)]struct NewTodo { title: String,}
fn main() { let todo = Todo { id: 1, title: String::from("Aprender axum"), done: false }; println!("{}", serde_json::to_string(&todo).unwrap());
let list = vec![todo.clone(), Todo { id: 2, title: String::from("Comprar leche"), done: true }]; println!("{}", serde_json::to_string(&list).unwrap());
let input: NewTodo = serde_json::from_str(r#"{"title":"Pasear al perro"}"#).unwrap(); println!("{:?}", input);
let bad = serde_json::from_str::<NewTodo>(r#"{"name":"Pasear al perro"}"#); println!("{}", bad.unwrap_err());}Salida:
{"id":1,"title":"Aprender axum","done":false}[{"id":1,"title":"Aprender axum","done":false},{"id":2,"title":"Comprar leche","done":true}]NewTodo { title: "Pasear al perro" }missing field `title` at line 1 column 26Un Vec<Todo> se convierte en un array JSON. Eso es exactamente lo que devolverá GET /todos. La última línea es el mensaje de error de serde (en inglés): falta el campo title.
Paso 3: Parámetros de ruta
Sección titulada «Paso 3: Parámetros de ruta»En una ruta como /todos/2, el 2 cambia de una petición a otra. En axum 0.8 marcas esa parte de la ruta con llaves, {id}, y la lees en el handler con el extractor Path:
use axum::{extract::Path, routing::get, Router};
#[tokio::main]async fn main() { let app = Router::new() .route("/hello/{name}", get(hello_name)) .route("/add/{a}/{b}", get(add));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Escuchando en http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn hello_name(Path(name): Path<String>) -> String { format!("¡Hola, {name}!")}
async fn add(Path((a, b)): Path<(i32, i32)>) -> String { format!("{a} + {b} = {}", a + b)}Un extractor es un parámetro del handler que saca una pieza de la petición. Path(name): Path<String> es un patrón: desenvuelve el Path y te da el String que hay dentro. Con dos parámetros, obtienes una tupla.
curl http://127.0.0.1:3000/hello/Anacurl http://127.0.0.1:3000/add/2/3¡Hola, Ana!2 + 3 = 5Aquí viene una sorpresa agradable. Dijiste que a y b son i32, así que axum lo comprueba por ti. Envía algo que no sea un número:
curl -i http://127.0.0.1:3000/add/2/xHTTP/1.1 400 Bad Requestcontent-type: text/plain; charset=utf-8content-length: 68
Invalid URL: Cannot parse value at index 1 with value `x` to a `i32`El mensaje (en inglés) dice que la URL no es válida porque x no se puede convertir en un i32. Tu handler ni siquiera llegó a ejecutarse. Los tipos de la firma de tu función son la validación.
Paso 4: Aceptar JSON con POST
Sección titulada «Paso 4: Aceptar JSON con POST»Para crear una tarea, el cliente envía JSON en el cuerpo (body) de la petición. El extractor Json lee el cuerpo y lo deserializa en tu struct. El cliente solo envía un título, así que usamos un struct aparte, NewTodo, para la entrada:
use axum::{routing::post, Json, Router};use serde::{Deserialize, Serialize};
#[derive(Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[derive(Deserialize)]struct NewTodo { title: String,}
#[tokio::main]async fn main() { let app = Router::new().route("/echo", post(echo_todo));
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Escuchando en http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
async fn echo_todo(Json(input): Json<NewTodo>) -> Json<Todo> { let todo = Todo { id: 1, title: input.title, done: false, }; Json(todo)}Usa post(...) en lugar de get(...) en la ruta. Con curl, -X POST elige el método, -H añade una cabecera y -d envía el cuerpo:
curl -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"title":"Comprar leche"}'{"id":1,"title":"Comprar leche","done":false}Otra vez, axum comprueba la entrada antes de que se ejecute tu código. Si al JSON le falta title, obtienes 422 Unprocessable Entity:
curl -i -X POST http://127.0.0.1:3000/echo -H "Content-Type: application/json" -d '{"name":"Comprar leche"}'HTTP/1.1 422 Unprocessable Entitycontent-type: text/plain; charset=utf-8content-length: 99
Failed to deserialize the JSON body into the target type: missing field `title` at line 1 column 24El mensaje dice que no se pudo deserializar el cuerpo JSON porque falta el campo title. Y si olvidas la cabecera Content-Type, obtienes 415 Unsupported Media Type con el mensaje Expected request with `Content-Type: application/json` . Esa cabecera es lo que más se olvida al probar con curl.
Paso 5: Estado compartido para una API de tareas de verdad
Sección titulada «Paso 5: Estado compartido para una API de tareas de verdad»Hasta ahora, cada handler se inventaba sus datos. Una API real necesita recordar las tareas entre peticiones. Las peticiones pueden llegar a la vez y se atienden en hilos distintos, así que la lista tiene que poder compartirse de forma segura. Ese es el trabajo de la pareja que conociste en la página de concurrencia:
Mutex<Vec<Todo>>se asegura de que solo una petición cambie la lista cada vez.Arc<...>permite que muchos handlers tengan un puntero a esa misma lista (consulta Smart pointers).
Le damos al tipo un nombre corto con type Db = Arc<Mutex<Vec<Todo>>>;, se lo pasamos al router con .with_state(db) y lo leemos en los handlers con el extractor State. axum clona el Arc en cada petición (es barato: solo copia un puntero).
Aquí tienes el programa completo. Sustituye src/main.rs por él:
use std::sync::{Arc, Mutex};
use axum::{ extract::{Path, State}, http::StatusCode, routing::get, Json, Router,};use serde::{Deserialize, Serialize};
#[derive(Clone, Serialize)]struct Todo { id: u32, title: String, done: bool,}
#[derive(Deserialize)]struct NewTodo { title: String,}
// La "base de datos": una lista de tareas que todas las peticiones comparten.type Db = Arc<Mutex<Vec<Todo>>>;
#[tokio::main]async fn main() { let db: Db = Arc::new(Mutex::new(Vec::new()));
let app = Router::new() .route("/todos", get(list_todos).post(create_todo)) .route("/todos/{id}", get(get_todo).delete(delete_todo)) .with_state(db);
let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); println!("Escuchando en http://127.0.0.1:3000"); axum::serve(listener, app).await.unwrap();}
// GET /todosasync fn list_todos(State(db): State<Db>) -> Json<Vec<Todo>> { let todos = db.lock().unwrap(); Json(todos.clone())}
// GET /todos/{id}async fn get_todo(State(db): State<Db>, Path(id): Path<u32>) -> Result<Json<Todo>, StatusCode> { let todos = db.lock().unwrap(); match todos.iter().find(|todo| todo.id == id) { Some(todo) => Ok(Json(todo.clone())), None => Err(StatusCode::NOT_FOUND), }}
// POST /todosasync fn create_todo(State(db): State<Db>, Json(input): Json<NewTodo>) -> (StatusCode, Json<Todo>) { let mut todos = db.lock().unwrap(); let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1; let todo = Todo { id, title: input.title, done: false, }; todos.push(todo.clone()); (StatusCode::CREATED, Json(todo))}
// DELETE /todos/{id}async fn delete_todo(State(db): State<Db>, Path(id): Path<u32>) -> StatusCode { let mut todos = db.lock().unwrap(); let before = todos.len(); todos.retain(|todo| todo.id != id); if todos.len() < before { StatusCode::NO_CONTENT } else { StatusCode::NOT_FOUND }}Repasemos las ideas nuevas:
- Una ruta, varios métodos.
get(list_todos).post(create_todo)mandaGET /todosyPOST /todosa handlers distintos. - Los códigos de estado son valores de retorno.
StatusCodeviene del módulohttpde axum. Un handler puede devolver unResult:Ok(Json(todo))se convierte en un 200 con JSON, yErr(StatusCode::NOT_FOUND)en un 404 vacío. Una tupla(StatusCode::CREATED, Json(todo))pone el estado y el cuerpo. Devolver soloStatusCode::NO_CONTENT(204) significa “hecho, no hay nada que devolver”. lock()y el guard.db.lock().unwrap()espera hasta que ninguna otra petición esté usando la lista y luego te da un guard (guardián) que puedes leer y cambiar como unVecnormal. El candado se libera cuando el guard sale del ámbito, al final de la función.- Ids nuevos. El siguiente id es uno más que el id más grande de la lista, o 1 si la lista está vacía.
- Borrar.
retainse queda con todas las tareas cuyo id no coincide. Si la longitud no cambió, no se borró nada, así que devolvemos 404.
¿Por qué std::sync::Mutex y no uno asíncrono? Está bien (y es más rápido) siempre que no mantengas el candado a través de un .await. Ninguno de nuestros handlers lo hace: bloquean, trabajan con la lista y devuelven.
La lógica de la lista y el candado no necesita un servidor web, así que puedes probarla en el Playground. Estas funciones hacen el mismo trabajo que los handlers, sin axum:
use std::sync::{Arc, Mutex};
#[derive(Clone, Debug)]struct Todo { id: u32, title: String, done: bool,}
type Db = Arc<Mutex<Vec<Todo>>>;
fn create(db: &Db, title: &str) -> Todo { let mut todos = db.lock().unwrap(); let id = todos.iter().map(|todo| todo.id).max().unwrap_or(0) + 1; let todo = Todo { id, title: title.to_string(), done: false }; todos.push(todo.clone()); todo}
fn get(db: &Db, id: u32) -> Option<Todo> { let todos = db.lock().unwrap(); todos.iter().find(|todo| todo.id == id).cloned()}
fn delete(db: &Db, id: u32) -> bool { let mut todos = db.lock().unwrap(); let before = todos.len(); todos.retain(|todo| todo.id != id); todos.len() < before}
fn main() { let db: Db = Arc::new(Mutex::new(Vec::new())); let second_handle = Arc::clone(&db); // lo que hace axum en cada petición
println!("{:?}", create(&db, "Comprar leche")); println!("{:?}", create(&second_handle, "Aprender axum")); println!("{:?}", get(&db, 2)); println!("{:?}", get(&db, 99)); println!("¿borrada la 1? {}", delete(&db, 1)); println!("¿borrada la 1 otra vez? {}", delete(&db, 1)); println!("quedan: {}", db.lock().unwrap().len());}Salida:
Todo { id: 1, title: "Comprar leche", done: false }Todo { id: 2, title: "Aprender axum", done: false }Some(Todo { id: 2, title: "Aprender axum", done: false })None¿borrada la 1? true¿borrada la 1 otra vez? falsequedan: 1La tarea creada a través de second_handle aparece cuando lees a través de db, porque los dos Arc apuntan a la misma lista.
Paso 6: Pruébalo con curl
Sección titulada «Paso 6: Pruébalo con curl»Arranca el servidor con cargo run y, en una segunda terminal, prueba todos los endpoints. Al principio la lista está vacía:
curl http://127.0.0.1:3000/todos[]Crea una tarea. Fíjate en el estado 201 Created:
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Comprar leche"}'HTTP/1.1 201 Createdcontent-type: application/jsoncontent-length: 45
{"id":1,"title":"Comprar leche","done":false}Crea una segunda tarea y lístalas todas:
curl -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title":"Aprender axum"}'curl http://127.0.0.1:3000/todos{"id":2,"title":"Aprender axum","done":false}[{"id":1,"title":"Comprar leche","done":false},{"id":2,"title":"Aprender axum","done":false}]Pide una por su id, y pide una que no existe:
curl http://127.0.0.1:3000/todos/2curl -i http://127.0.0.1:3000/todos/99{"id":2,"title":"Aprender axum","done":false}HTTP/1.1 404 Not Foundcontent-length: 0Borra la tarea 1 e intenta borrarla otra vez:
curl -i -X DELETE http://127.0.0.1:3000/todos/1curl -i -X DELETE http://127.0.0.1:3000/todos/1HTTP/1.1 204 No Content
HTTP/1.1 404 Not Foundcontent-length: 0curl http://127.0.0.1:3000/todos[{"id":2,"title":"Aprender axum","done":false}]axum también gestiona algunos errores por ti. Un tipo equivocado en el JSON da un 422 que explica el problema, y un método que no configuraste para una ruta da 405 Method Not Allowed, con una cabecera que lista los métodos que sí funcionan:
curl -i -X POST http://127.0.0.1:3000/todos -H "Content-Type: application/json" -d '{"title": 42}'curl -i -X PUT http://127.0.0.1:3000/todos/2HTTP/1.1 422 Unprocessable Entitycontent-type: text/plain; charset=utf-8content-length: 130
Failed to deserialize the JSON body into the target type: title: invalid type: integer `42`, expected a string at line 1 column 12HTTP/1.1 405 Method Not Allowedallow: GET,HEAD,DELETEcontent-length: 0El 422 dice que title tiene un tipo no válido: llegó el entero 42 y se esperaba un texto. Detén el servidor con Ctrl + C y vuelve a arrancarlo: la lista está vacía. Los datos solo viven en memoria. Guardarlos es la primera idea de más abajo.
Errores comunes
Sección titulada «Errores comunes»Usar la sintaxis antigua :id en las rutas. Muchos tutoriales de internet se escribieron para axum 0.7, que usaba /todos/:id. En axum 0.8 esto compila, pero el programa hace panic nada más arrancar:
thread 'main' (12345) panicked at src/main.rs:32:10:Path segments must not start with `:`. For capture groups, use `{capture}`. If you meant to literally match a segment starting with a colon, call `without_v07_checks` on the router.El mensaje dice que los segmentos de la ruta no pueden empezar por : y que uses {capture}. La solución: escribe /todos/{id}.
Poner Json antes que otros extractores. El cuerpo de la petición solo se puede leer una vez, así que el extractor que lo lee (Json) tiene que ser el último parámetro. Si escribes create_todo(Json(input): Json<NewTodo>, State(db): State<Db>), el error no lo dice directamente. Las líneas importantes son:
error[E0277]: the trait bound `fn(Json<NewTodo>, State<...>) -> ... {create_todo}: Handler<_, ...>` is not satisfied |31 | .route("/todos", get(list_todos).post(create_todo)) | ---- ^^^^^^^^^^^ unsatisfied trait bound | = note: Consider using `#[axum::debug_handler]` to improve the error messageCuando veas “Handler is not satisfied” (no se cumple el trait Handler), revisa el orden de tus parámetros, y comprueba que cada tipo de retorno sea algo que axum sepa convertir en una respuesta.
Olvidar -H "Content-Type: application/json" en curl. El servidor responde 415. El extractor Json necesita esa cabecera.
Olvidar reiniciar. El servidor que está funcionando es el programa antiguo. Detenlo con Ctrl + C y vuelve a hacer cargo run después de cada cambio.
Ideas para ampliarlo
Sección titulada «Ideas para ampliarlo»Cuando la API básica funcione, aquí tienes buenos pasos siguientes. Pruébalos de uno en uno.
- Marcar una tarea como hecha. Añade
PATCH /todos/{id}con.patch(...)en la segunda ruta. Busca la tarea coniter_mut(), pondone = truey devuélvela. - Validación. No debería permitirse un título vacío. Un handler puede devolver
Result<(StatusCode, Json<Todo>), (StatusCode, String)>y responder conErr((StatusCode::BAD_REQUEST, String::from("el título no puede estar vacío"))). Usatitle.trim().is_empty()para comprobarlo. - Ids que nunca se repiten. Con “el id más grande más uno”, borrar la tarea más reciente permite que su id se vuelva a usar. Guarda en su lugar un contador aparte en tu estado, por ejemplo un struct con
todos: Vec<Todo>ynext_id: u32dentro delMutex. - Persistencia con un archivo JSON. Deriva también
DeserializeenTodo. Al arrancar, leetodos.jsonconstd::fs::read_to_stringyserde_json::from_str(o empieza con la lista vacía si el archivo no existe). Después de cada cambio, vuelve a escribir la lista conserde_json::to_string_prettyystd::fs::write. Las páginas de lectura y escritura de archivos y JSON con Serde tienen todo lo que necesitas. Más adelante puedes cambiar el archivo por una base de datos de verdad, como SQLite. - Logging con tower-http. Ejecuta
cargo add tower-http --features traceycargo add tracing-subscriber, y añade una capa (layer) de logging. Ahora el servidor imprime una línea por cada petición, con el método, la ruta, el estado y cuánto tardó (consulta Logging en Rust):
use tower_http::trace::TraceLayer;
// al principio de main:tracing_subscriber::fmt() .with_max_level(tracing_subscriber::filter::LevelFilter::DEBUG) .init();
// al construir el router:let app = Router::new() .route("/todos", get(list_todos).post(create_todo)) .route("/todos/{id}", get(get_todo).delete(delete_todo)) .layer(TraceLayer::new_for_http()) .with_state(db);- Notas para desplegarlo.
127.0.0.1solo acepta peticiones de tu propio ordenador. Para aceptar peticiones de otras máquinas (por ejemplo dentro de Docker o en un servidor), usa0.0.0.0en su lugar. Muchos servicios de hosting le dicen a tu programa qué puerto usar mediante una variable de entornoPORT, que puedes leer constd::env::var(consulta Entorno y procesos). Compila concargo build --releasepara tener un servidor mucho más rápido. El resultado es un único archivo entarget/release/que puedes copiar a un servidor Linux o meter en una imagen pequeña de Docker. Para un sitio público, ponlo detrás de un proxy inverso (como Caddy o nginx) que se encargue de HTTPS.
Resumen
Sección titulada «Resumen»- Un handler es una
async fn. Un router asocia un método y una ruta con un handler.axum::servelo ejecuta sobre unTcpListenerdetokio. - Los extractores sacan datos de la petición:
Pathpara partes de la URL (/todos/{id}),Jsonpara el cuerpo (siempre el último) yStatepara los datos compartidos. - Devuelve
Json(value)para JSON,StatusCodepara un estado, una tupla para poner los dos, o unResultpara elegir entre éxito y un estado de error. Arc<Mutex<Vec<Todo>>>comparte una lista de forma segura entre todas las peticiones. Bloquéala, úsala y deja que el guard se destruya. No mantengas el candado a través de un.await.- Prueba tu API con
curl:-imuestra el estado,-Xelige el método,-Hañade una cabecera y-denvía un cuerpo.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. En axum 0.8, ¿cómo escribes una ruta con un id en el camino?
2. Un handler devuelve (StatusCode::CREATED, Json(todo)). ¿Qué recibe el cliente?
3. ¿Por qué la lista compartida va envuelta en Arc<Mutex<...>>?
4. ¿Qué extractor tiene que ir el último en los parámetros de un handler?
5. Haces curl -X POST con algo de JSON pero olvidas la cabecera Content-Type: application/json. ¿Qué responde axum?
Puntuación: 0 / 5
Para leer más
Sección titulada «Para leer más»- Documentación de axum — docs.rs (en inglés): la guía oficial, con secciones sobre rutas, handlers y respuestas.
- Extractores de axum — docs.rs (en inglés): todos los extractores incluidos y las reglas sobre su orden.
- Tutorial de Tokio (en inglés): aprende el runtime asíncrono que mueve axum, incluido el estado compartido.
- Documentación de tower-http — docs.rs (en inglés): capas listas para usar para logging, CORS, compresión y tiempos de espera.
- Códigos de estado de respuesta HTTP — MDN: qué significan 200, 201, 204, 404, 422 y los demás, en español.