← Table of contents

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:

PathVerbOperation
/tasksPOSTadd a task
/tasksGETlist all tasks
/tasks/{id}GETget one task
/tasks/{id}/donePATCHcomplete 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 tq code for this chapter is in 6-the-port/03-the-four-verbs/.


Lore: HTTP Verbs

The REST convention assigns meaning to each verb:

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.