6.3 The Four Verbs
A craftsman who can only respond is not yet a craftsman - he is a person who is home. A name exists, a place exists; the trade has not been announced. A visitor with business - to add, to find, to mark - will leave empty-handed. Not because they were refused. Because no agreement was reached.
What You Need
An HTTP API is a contract: the client names a verb and a path, the server answers. The four
tq operations get four addresses:
| Path | Verb | Operation |
|---|---|---|
/tasks | POST | add a task |
/tasks | GET | list all tasks |
/tasks/{id} | GET | get one task |
/tasks/{id}/done | PATCH | complete a task |
The verbs are not chosen arbitrarily - each carries meaning: GET reads without changing state; POST creates; PATCH updates part of a resource. More in the Lore section of this chapter.
The handlers will be stubs for now: strings instead of data. Step by step they will be given real content.
The Build
The routes live in a separate file - this keeps crates/api/src/main.rs concerned only
with starting the server, and the routes can be read and changed independently.
Create crates/api/src/routes.rs:
// crates/api/src/routes.rs - NEW
use axum::{
Router,
routing::{get, patch, post},
};
async fn add() -> String {
"TODO: add task".to_string()
}
async fn list() -> String {
"TODO: list tasks".to_string()
}
async fn get_task() -> String {
"TODO: get task".to_string()
}
async fn done() -> String {
"TODO: mark done".to_string()
}
pub fn router() -> Router {
Router::new()
.route("/tasks", post(add).get(list))
.route("/tasks/{id}", get(get_task))
.route("/tasks/{id}/done", patch(done))
}
.route("/tasks", post(add).get(list)) - two actions on one path: POST /tasks creates,
GET /tasks returns the list.
{id} is a path parameter: axum will extract the number from the URL; the stubs do not use
it yet.
Update crates/api/src/main.rs. health stays here - it is a server route, not a
resource route: it is not about tasks, but about whether the process is alive:
// crates/api/src/main.rs - CHANGED
mod routes;
use axum::{Router, routing::get};
async fn health() -> String {
"tq ok".to_string()
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/", get(health))
.merge(routes::router());
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
.merge(routes::router()) adds all routes from routes.rs to app.
The Result
$ make serve
In another terminal:
$ curl -X POST localhost:3000/tasks
TODO: add task
$ curl localhost:3000/tasks
TODO: list tasks
$ curl localhost:3000/tasks/1
TODO: get task
$ curl -X PATCH localhost:3000/tasks/1/done
TODO: mark done
Four routes respond. No data yet - the API shape is ready.
make ci passes: the same 38 tests.
The complete
tqcode for this chapter is in6-the-port/03-the-four-verbs/.
Lore: HTTP Verbs
The REST convention assigns meaning to each verb:
- GET - read without changing state. Two identical
GET /taskscalls return the same result. A browser may cache it; a client may retry without risk. - POST - create a new resource. Two identical
POST /taskscalls create two tasks. - PATCH - partially update a resource.
PATCH /tasks/{id}/donechanges only the status. - PUT - replace a resource entirely.
tqdoes not use it: a task is not overwritten, only updated. - DELETE - remove. In
tqtasks are not deleted - only completed. If deletion were added, it would beDELETE /tasks/{id}.
Following these conventions is not required. But a client that knows REST also knows what
to expect from GET and POST without documentation. The convention works in both
directions.