# Handling Commands over HTTP

To accept a command over HTTP, define a request type that derives `Deserialize` for the body, and a handler of axum that builds the command from it, executes it, and answers. The handler takes the body with the `Strict` extractor, the user with the extractor of your application (see **[Setting Up an HTTP API](/docs/architecturekit/rust/setting-up-an-http-api)**), and what the body does not hold from other extractors, such as a value of the path with `Path`, as a handler of a query does (see **[Handling Queries over HTTP](/docs/architecturekit/rust/handling-queries-over-http)**).

For example, for the `BorrowBook` that checks the revision of the caller (see **[Checking the Revision of the Caller](/docs/architecturekit/rust/using-preconditions#checking-the-revision-of-the-caller)**), with the ID of the book in the path, and the date checked with `NaiveDate` of the `chrono` crate, which you add with `cargo add chrono`:

```rust
use architecturekit::httpapi::{CommandAnswer, HttpError, Strict};
use axum::extract::{Path, State};
use chrono::NaiveDate;
use serde::Deserialize;

#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct BorrowBookRequest {
  borrowed_until: String,
  expected_event_id: String,
}

pub async fn handle_borrow_book(
  State(app): State<App>,
  Path(book_id): Path<String>,
  user: User,
  Strict(request): Strict<BorrowBookRequest>,
) -> Result<CommandAnswer, HttpError> {
  BOOKS.check(&[&book_id])?;
  if NaiveDate::parse_from_str(&request.borrowed_until, "%Y-%m-%d").is_err() {
    return Err(HttpError::malformed("borrowedUntil must be a date"));
  }
  let Ok(expected_event_id) = request.expected_event_id.parse() else {
    return Err(HttpError::malformed("expectedEventId must be an event ID"));
  };

  let command = BorrowBook {
    book: BOOKS.build(&[&book_id]),
    reader_id: user.id,
    borrowed_until: request.borrowed_until,
    expected_event_id,
  };
  let written = app.store.execute(&app.borrow_book, command).await?;

  Ok(CommandAnswer::new(&written))
}
```

**The handler is the place to validate a request**, since `HttpError::malformed` answers with `400 Bad Request` and `malformed request: ` followed by the reason. Check at least what would otherwise fail later: the ID of the book becomes part of a subject, and `build` panics on an empty ID or one with a character that a segment of a subject may not contain, such as a slash or a dot (see **[Composing Subjects](/docs/architecturekit/rust/composing-subjects)**), which is answered with `500 Internal Server Error` at best (see below). `check` refuses such an ID with an error that `?` answers with `400 Bad Request` and its own text, such as `value "a.b" for "book_id" may only contain A-Z, a-z, 0-9, underscores, and hyphens`. A value of the path is no exception, since it may hold a slash, sent as `%2F`. And a value that does not match the schema of its event is refused by the database, which is a permanent failure answered with `500 Internal Server Error` – although it is the caller's mistake. The expected event ID becomes a `Revision`, which `on_event_id` takes, so the handler reads it with `parse`. Handing on its error with `?` would answer with `400 Bad Request` as well, with a message such as `not a revision: "abc"`. Refusing it here lets the message name the field.

Then route the handler with the router:

```rust
use axum::routing::post;

let router = Router::new().route("/api/books/{id}/borrow", post(handle_borrow_book));
```

A command changes something, so route it with a method that may do so, usually `post`. `GET`, `HEAD`, `OPTIONS`, and `QUERY` must not change anything. A browser sends `GET` and `HEAD` from another site without asking, for example for a link that the user follows, along with the cookies of the user, and a route that takes no body lets `GET`, `HEAD`, and `OPTIONS` pass from another origin (see **[Handling Commands Without a Body](/docs/architecturekit/rust/handling-commands-over-http#handling-commands-without-a-body)**). `QUERY` may change nothing either, since HTTP defines it as safe and repeatable, so a client or a cache may send it again. **Routing is up to axum, so nothing keeps a command from being routed with `get` or `any`**: route it with `post`, rather than executing the command for any site that links to it, or whenever a query is sent again.

The handler decodes the request body, builds the command, and executes it:

```shell
curl -X POST http://localhost:8080/api/books/42/borrow \
  -H "Authorization: Bearer …" \
  -H "Content-Type: application/json" \
  -d '{"borrowedUntil":"2026-10-24","expectedEventId":"0"}'
```

If this succeeds, `CommandAnswer` answers with `200 OK` and the revision it has written, which is the ID of the last written event:

```json
{"revision":"1"}
```

The revision is empty if the command did not write anything. A caller hands it to a query to read its own writes (see **[Reading Your Own Writes over HTTP](/docs/architecturekit/rust/reading-your-own-writes-over-http)**).

Otherwise, `HttpError` answers with the status code that matches the error (see **[Mapping Errors to Status Codes](/docs/architecturekit/rust/mapping-errors-to-status-codes)**) and a message:

```json
{"message":"/books/42 is already borrowed"}
```

The message is the error message if the error is written for the caller, such as a broken business rule or what is wrong with the request. Other errors may name internals, such as the key a token failed to verify with, or the subject a precondition guarded, so their message is fixed:

| Status code | Message |
|---|---|
| `401 Unauthorized` | `unauthorized` |
| `404 Not Found`, for a query that found no item | `not found` |
| `409 Conflict` | `conflict: the data has changed since it was read` |
| `500 Internal Server Error`, for a write of the command whose outcome is unknown | `outcome unknown: the request may have succeeded` |
| `500` and above | `internal server error` |

The actual error is logged, so that it does not vanish (see **[Setting Up an HTTP API](/docs/architecturekit/rust/setting-up-an-http-api)**): at level `info` for `401`, `404`, and `409`, since the server did not fail, and at level `error` for `500` and above. For `404`, that holds only for a query that found no item (see **[Reporting Missing Items](/docs/architecturekit/rust/handling-queries-over-http#reporting-missing-items)**). Every answer to a command, whether a success or a failure, says `Cache-Control: no-store`, so that no cache keeps it.

If a handler panics, for example because `build` received an ID that was not checked, axum would drop the connection, and the caller would get no answer at all. **A handler belongs to the application, so the kit catches no panic itself.** Add the layer `CatchPanicLayer` of tower-http with `answer_panic` to the router instead (see **[Setting Up an HTTP API](/docs/architecturekit/rust/setting-up-an-http-api)**). It answers with `500 Internal Server Error` and the message `internal server error`, like any other internal failure, and logs the panic at level `error`, with its message.

An error of the kit turns into an `HttpError` with `?`, by what it is (see **[Mapping Errors to Status Codes](/docs/architecturekit/rust/mapping-errors-to-status-codes)**). An error of your own turns into one with `HttpError::of`, which knows only its category, so it answers an error without a category with `500 Internal Server Error`, also if the handler has found a mistake in the request itself. Answer such a mistake with `HttpError::malformed`, as above, to answer it with `400 Bad Request`.

*Note that the request type only describes the body, so it may come from another crate, for example one that the application shares with its clients.*

## Handling Commands Without a Body

Some commands take everything they need from the path, such as `ReturnBook`, which only needs the ID of the book. Such a command takes no body, so its handler takes the `NoBody` extractor instead of `Strict`:

```rust
use architecturekit::httpapi::NoBody;

pub async fn handle_return_book(
  State(app): State<App>,
  Path(book_id): Path<String>,
  _: User,
  _: NoBody,
) -> Result<CommandAnswer, HttpError> {
  BOOKS.check(&[&book_id])?;

  let written =
    app.store.execute(&app.return_book, ReturnBook { book: BOOKS.build(&[&book_id]) }).await?;

  Ok(CommandAnswer::new(&written))
}
```

```rust
let router = Router::new().route("/api/books/{id}/return", post(handle_return_book));
```

The handler then expects no body, so the caller sends neither a `Content-Type` header nor a body:

```shell
curl -X POST http://localhost:8080/api/books/42/return \
  -H "Authorization: Bearer …"
```

A body of `{}` is accepted as well, whatever the `Content-Type` header says, so that a caller that sends one out of habit keeps working. Any other body is answered with `400 Bad Request`, and the message says that the route takes no body.

**Requiring `application/json` is what keeps a browser from sending a command from another site without asking the server first**, since a form can not send JSON. A route without a body can not rely on that, so before it looks at the body, `NoBody` checks where the request comes from. A request that a browser sends from another origin, which the `Sec-Fetch-Site` header says, or, without it, an `Origin` header whose host differs from the `Host` header, is answered with `403 Forbidden`, and the message `forbidden: a request without a body is not accepted from another origin`. A request from the same origin passes, and so does one without these headers, such as one of `curl` or of another server.

*Note that this also refuses a browser frontend that runs on another origin than the API, even on another subdomain, so such a frontend can not call a route that takes `NoBody`.*

The check lets a request with `GET`, `HEAD`, or `OPTIONS` pass from any origin, since these methods must not change anything. That is why a handler that executes a command is routed with a method that may change something, such as `post` (see above).

## Adding to the Answer

To answer with more than the revision, for example with the ID that the handler below makes up for a new book, call the `with` method on the answer. It takes the fields to add, usually as a struct that derives `Serialize`. Here, the ID comes from the uuid crate, which you add with `cargo add uuid --features v4`:

```rust
use serde::Serialize;
use uuid::Uuid;

#[derive(Deserialize)]
pub struct AcquireBookRequest {
  title: String,
  author: String,
  isbn: String,
}

#[derive(Serialize)]
pub struct AcquiredBook {
  id: String,
}

pub async fn handle_acquire_book(
  State(app): State<App>,
  _: User,
  Strict(request): Strict<AcquireBookRequest>,
) -> Result<CommandAnswer, HttpError> {
  let book_id = Uuid::new_v4().to_string();
  let command = AcquireBook {
    book: BOOKS.build(&[&book_id]),
    title: request.title,
    author: request.author,
    isbn: request.isbn,
  };
  let written = app.store.execute(&app.acquire_book, command).await?;

  Ok(CommandAnswer::new(&written).with(AcquiredBook { id: book_id }))
}
```

```rust
let router = Router::new().route("/api/acquire-book", post(handle_acquire_book));
```

The handler then answers with both:

```json
{"id":"…","revision":"0"}
```

The fields are only added once the command has succeeded, since `?` returns the failure before. A value that encodes to `null`, such as `None`, adds no fields, and calling `with` again replaces the fields of an earlier call.

If the fields come from something that may fail after the command has succeeded, such as a view that has to catch up with the events first, hand over the result with the `with_result` method instead (see **[Waiting for Written Events](/docs/architecturekit/rust/reading-your-own-writes#waiting-for-written-events)**). If it is an error, **the events are written all the same, so the handler still answers with `200 OK` and the revision**, which the caller needs to read its own writes, and must not take for a reason to send the command again. The answer then holds no further fields, and the error is logged at level `error`.

The kit adds the revision itself, so the fields must not contain one, and they must encode to a JSON object. Otherwise, or if they can not be encoded at all, the answer is `500 Internal Server Error`, which is logged, although the events have been written, since that is a mistake in the code rather than something that happens at runtime.

*Note that the written events are available to the handler as well, in what `execute` returns. Add them only deliberately: they are the inner model of the application, every caller that reads them depends on their shape, and they may contain data that is not meant for the caller.*

## Answering Commands in Your Own Format

To answer in a format of your own, for example with another status code, return whatever axum can turn into a response, and keep `HttpError` for the failures:

```rust
use axum::{Json, http::StatusCode};

pub async fn handle_acquire_book(
  State(app): State<App>,
  _: User,
  Strict(request): Strict<AcquireBookRequest>,
) -> Result<(StatusCode, Json<AcquiredBook>), HttpError> {
  let book_id = Uuid::new_v4().to_string();
  let command = AcquireBook {
    book: BOOKS.build(&[&book_id]),
    title: request.title,
    author: request.author,
    isbn: request.isbn,
  };
  app.store.execute(&app.acquire_book, command).await?;

  Ok((StatusCode::CREATED, Json(AcquiredBook { id: book_id })))
}
```

*Note that such an answer carries no revision, so its caller can not read its own writes. To add it, take it from the written events with `revision_of`, which returns `None` if the command wrote nothing.*

*Note that such an answer carries no `Cache-Control` header, unless the handler adds one, while every answer of the kit to a command says `no-store`.*

## Authorizing Commands

To refuse a command, return `HttpError::forbidden` from the handler before it executes the command. The request is then answered with `403 Forbidden`, and `forbidden: ` followed by the reason:

```rust
pub async fn handle_acquire_book(
  State(app): State<App>,
  user: User,
  Strict(request): Strict<AcquireBookRequest>,
) -> Result<CommandAnswer, HttpError> {
  if !user.is_librarian {
    return Err(HttpError::forbidden("only librarians may acquire books"));
  }

  let book_id = Uuid::new_v4().to_string();
  let command = AcquireBook {
    book: BOOKS.build(&[&book_id]),
    title: request.title,
    author: request.author,
    isbn: request.isbn,
  };
  let written = app.store.execute(&app.acquire_book, command).await?;

  Ok(CommandAnswer::new(&written).with(AcquiredBook { id: book_id }))
}
```

The same applies to every other refusal a handler returns before it executes the command, such as `HttpError::not_found`, or an error of the category `Category::Domain`, which `HttpError::of` answers with `422 Unprocessable Entity` and its own text. If the handler looks something up in another service, and that service is down, hand the error, which belongs to the category `Category::Transient`, to `HttpError::of`. The request is then answered with `503 Service Unavailable`, and the failure is logged, rather than blaming the request.

## Validating Requests

Before the handler receives the body, `Strict` validates the request, as it does for a query (see **[Handling Queries over HTTP](/docs/architecturekit/rust/handling-queries-over-http)**):

- The `Content-Type` header must be `application/json`, otherwise the request is answered with `415 Unsupported Media Type`, and the message starts with `unsupported media type: `. Parameters such as `charset` may follow, and case does not matter.
- The body must not be larger than `MAX_REQUEST_BODY`, which is one mebibyte, otherwise the request is answered with `413 Payload Too Large`, and the message starts with `request body too large: `. The same goes for a body that a layer cuts off at a lower limit, such as `DefaultBodyLimit` of axum or `RequestBodyLimitLayer` of tower-http.
- The body must be a single valid JSON value that fits the request type, otherwise the request is answered with `400 Bad Request`, and the message starts with `malformed request: `. **A field that the request type does not have is refused at any depth**, without `#[serde(deny_unknown_fields)]`. Nothing but whitespace may follow the value, and no name may occur twice in an object. A list where a struct belongs is refused as well, which serde would otherwise take for the values of the fields, in their order, so that a caller who put them in another order would set the wrong fields. A field that the body leaves out is refused as missing, unless the request type gives it a default, such as an `Option`, or with `#[serde(default)]`.

*Note that parsers disagree on what a name that occurs twice means, and on data after the value: one takes the first value, another the last, and one stops after the value, while another reads on. A filter or a proxy in front of the application might then check another value than the one the application uses, which is why both are refused.*

*Note that serde decodes an enum with `#[serde(untagged)]` or `#[serde(tag = "…")]`, and a field with `#[serde(flatten)]`, from a copy of its own that the checks do not reach, so within them, neither a field that the type does not have nor a list for a struct is refused.*

If the body can not be decoded, the message says what is wrong in words of its own, rather than in those of serde, which name the types of Rust and positions in the text. After `malformed request: `, it says, for example, `empty body`, `invalid JSON`, `data after the JSON value`, `unknown field "borowedUntil"`, `duplicate field "expectedEventId"`, `"borrowedUntil" must be a string`, `"borrowedUntil" is missing`, `the body must be an object`, for a body of `null` or a list as well, or `"pair" must have 2 elements`. A nested value is named by its path, such as `"items.0.bookId"`. For a failure that it has no words of its own for, it says that the value can not be decoded, as in `"rating" can not be decoded`. A body that can not even be read, for example because a layer fails to decompress it, gives `the body could not be read`. The error of a type that decodes itself, on the other hand, keeps its own text, as it is. For the full list, see the **[documentation of `body_of`](https://docs.rs/architecturekit/latest/architecturekit/httpapi/fn.body_of.html)**.

If the handler takes `NoBody`, the request is validated differently (see **[Handling Commands Without a Body](/docs/architecturekit/rust/handling-commands-over-http#handling-commands-without-a-body)**): a request that a browser sends from another origin is answered with `403 Forbidden` first. The `Content-Type` header is not required, and the body must be empty or `{}`, otherwise the request is answered with `400 Bad Request`. A body larger than `MAX_REQUEST_BODY` is still answered with `413 Payload Too Large`.

Since `Strict` and `NoBody` read the body, they come last among the extractors of a handler, after the one for the user, so that an unknown caller is refused before the body is read. For a web framework other than axum, the feature `http` offers the same with the types of the `http` crate: `body_of` reads a body by the same rules as `Strict`, from the parts of the request and its bytes, and `no_body` checks a request as `NoBody` does.
