# Serving Commands and Queries over HTTP

In this step, you turn the library into an **HTTP API** that accepts commands and answers queries. The API keeps its view up to date while it runs, and lets a caller **read its own writes**.

## Mapping Requests to Commands

The commands from **[Making Decisions](/learn/tutorials/first-steps-with-architecturekit/making-decisions)** know nothing about HTTP. To accept them over HTTP, define a **request type** for each command, with JSON annotations, and implement the **`ToCommand`** function, which turns a request into a command.

Create a file `requests.go`:

```go
package main

import (
  "errors"
  "net/http"
  "time"

  "github.com/thenativeweb/architecturekit-golang/architecturekit/httpapi"
)

type acquireBookRequest struct {
  BookID string `json:"bookId"`
  Title  string `json:"title"`
  Author string `json:"author"`
  ISBN   string `json:"isbn"`
}

func (r acquireBookRequest) ToCommand(user httpapi.NoUser) (AcquireBook, error) {
  if err := bookSubject.Check(r.BookID); err != nil {
    return AcquireBook{}, err
  }

  return AcquireBook{
    BookID: r.BookID,
    Title:  r.Title,
    Author: r.Author,
    ISBN:   r.ISBN,
  }, nil
}

type borrowBookRequest struct {
  BookID        string `json:"bookId"`
  ReaderID      string `json:"readerId"`
  BorrowedUntil string `json:"borrowedUntil"`
}

func (r borrowBookRequest) ToCommand(user httpapi.NoUser) (BorrowBook, error) {
  if err := bookSubject.Check(r.BookID); err != nil {
    return BorrowBook{}, err
  }
  if _, err := time.Parse(time.DateOnly, r.BorrowedUntil); err != nil {
    return BorrowBook{}, errors.New("borrowedUntil must be a date such as 2026-10-24")
  }

  return BorrowBook{
    BookID:        r.BookID,
    ReaderID:      r.ReaderID,
    BorrowedUntil: r.BorrowedUntil,
  }, nil
}

type returnBookRequest struct {
  BookID string `json:"bookId"`
}

func (r returnBookRequest) ToCommand(user httpapi.NoUser) (ReturnBook, error) {
  if err := bookSubject.Check(r.BookID); err != nil {
    return ReturnBook{}, err
  }

  return ReturnBook{
    BookID: r.BookID,
  }, nil
}

func toListBooks(r *http.Request, user httpapi.NoUser) (ListBooks, error) {
  return ListBooks{
    OnlyAvailable: r.URL.Query().Get("available") == "true",
  }, nil
}
```

`ToCommand` receives the **user** who makes the request. The library does not authenticate its users, so the user is always **`httpapi.NoUser`**, and the reader who borrows a book is named in the request instead. For an API with authentication, see **[Setting Up an HTTP API](/docs/architecturekit/setting-up-an-http-api)**.

`ToCommand` is also the place to **validate** a request, since a plain error it returns is answered with `400 Bad Request` (for errors that keep a status code of their own, see **[Authorizing Commands](/docs/architecturekit/handling-commands-over-http#authorizing-commands)**). The functions check two things:

- The ID of a book becomes part of a subject, and composing a subject from an empty ID, or from one that contains a slash, is a programming error that makes `Build` panic. So the functions first call **`Check`** on the scheme of the subject, which returns an error for such an ID instead (see **[Composing Subjects](/docs/architecturekit/composing-subjects)**).
- EventSourcingDB would reject a return date that is not a date, since the schema of `BookBorrowed` requires one. But ArchitectureKit treats an event that does not match its schema as a failure that trying again will not fix, which is answered with `500 Internal Server Error`. Checking the date first turns a mistake of the caller into a `400 Bad Request`.

`toListBooks` does for the query what `ToCommand` does for commands: it turns a request into a `ListBooks` query, and reads the query parameter `available` for that.

## Wiring the API

Replace the content of `main.go` with the following program, which serves the three commands and the query on port `8080`:

```go
package main

import (
  "context"
  "log"
  "net/http"

  "github.com/thenativeweb/architecturekit-golang/architecturekit"
  "github.com/thenativeweb/architecturekit-golang/architecturekit/httpapi"
)

func main() {
  store, err := newStore()
  if err != nil {
    log.Fatal(err)
  }

  catalog := newCatalog()
  catalogProjection := architecturekit.Tracking(catalog, newCatalogProjection(catalog))

  run := architecturekit.StartProjection(context.TODO(), store, "/books", true, catalogProjection)

  select {
  case <-run.CaughtUp():
  case <-run.Done():
    log.Fatal(run.Err())
  }

  api := httpapi.NewPublicAPI(store)
  mux := http.NewServeMux()

  httpapi.Route[acquireBookRequest](api, mux, "POST /api/acquire-book", acquireBook)
  httpapi.Route[borrowBookRequest](api, mux, "POST /api/borrow-book", borrowBook)
  httpapi.Route[returnBookRequest](api, mux, "POST /api/return-book", returnBook)

  httpapi.QueryRevisioned(api, mux, "GET /api/books", catalog, toListBooks, listBooks(catalog), httpapi.DefaultWait)

  log.Println("Listening on http://localhost:8080.")
  log.Fatal(http.ListenAndServe(":8080", mux))
}
```

Unlike `CatchUpProjection`, **`StartProjection`** does not stop once it has applied the stored events. It runs the projection in the background and keeps **observing** new events, so the catalog follows every command the API executes. Since a half-built view would answer wrongly, the program waits until **`CaughtUp`** reports that the projection has applied all events that were stored when it started, and only then serves requests. If the projection ends before, **`Done`** says so, and the program stops.

**`Tracking`** wraps the projection, so that the catalog records the ID of the last event it has seen. That ID is the catalog's **revision**, and the query needs it to let callers read their own writes, as described below.

**`NewPublicAPI`** creates an API without authentication. **`Route`** accepts a command: it decodes the request, calls `ToCommand`, and executes the command with its decider. **`QueryRevisioned`** answers the query with `toListBooks` and `listBooks`.

Start the API:

```shell
go run .
```

It prints the following line, with the current date and time at the beginning:

```
2026/09/29 21:58:04 Listening on http://localhost:8080.
```

Open another terminal and ask for all books. The catalog has been built from the events of the previous steps:

```shell
curl http://localhost:8080/api/books
```

```json
[{"id":"42","title":"2001 – A Space Odyssey","author":"Arthur C. Clarke","isBorrowed":true,"borrowedUntil":"2026-10-24"}]
```

## Sending Commands

The library acquires a second copy of the book, with the ID `43`:

```shell
curl \
  -i \
  -X POST \
  -H "content-type: application/json" \
  -d '{"bookId":"43","title":"2001 – A Space Odyssey","author":"Arthur C. Clarke","isbn":"978-0756906788"}' \
  http://localhost:8080/api/acquire-book
```

The API answers with `200 OK` and the IDs of the written events. Apart from the `Date` header, which shows the current time in this and all following responses, it looks like this:

```
HTTP/1.1 200 OK
Content-Type: application/json
Date: Tue, 29 Sep 2026 21:58:18 GMT
Content-Length: 34

{"eventIds":["2"],"message":"ok"}
```

Now try to lend book `42` to a second reader:

```shell
curl \
  -i \
  -X POST \
  -H "content-type: application/json" \
  -d '{"bookId":"42","readerId":"17","borrowedUntil":"2026-10-31"}' \
  http://localhost:8080/api/borrow-book
```

The decider rejects the command, just as in **[Making Decisions](/learn/tutorials/first-steps-with-architecturekit/making-decisions)**, and the API answers with `422 Unprocessable Entity` and the message of the rejection:

```
HTTP/1.1 422 Unprocessable Entity
Content-Type: application/json
Date: Tue, 29 Sep 2026 21:58:19 GMT
Content-Length: 42

{"message":"book 42 is already borrowed"}
```

A request with a return date that is not a date does not even reach the decider:

```shell
curl \
  -i \
  -X POST \
  -H "content-type: application/json" \
  -d '{"bookId":"43","readerId":"17","borrowedUntil":"tomorrow"}' \
  http://localhost:8080/api/borrow-book
```

`ToCommand` refuses it, and the API answers with `400 Bad Request`:

```
HTTP/1.1 400 Bad Request
Content-Type: application/json
Date: Tue, 29 Sep 2026 21:58:19 GMT
Content-Length: 90

{"message":"httpapi: malformed request: borrowedUntil must be a date such as 2026-10-24"}
```

Every error maps to the status code that matches its category. For the complete list, see **[Mapping Errors to Status Codes](/docs/architecturekit/mapping-errors-to-status-codes)**.

## Reading Your Own Writes

The projection applies new events in the background, so the catalog lags a little behind the events that have been written. A query sent right after a command may therefore not see the command's events yet – the view is **[eventually consistent](/concepts/eventual-consistency)**.

To see its own write, a caller sends the highest ID from `eventIds` in the **`Wait-For-Revision`** header. The query then waits until the catalog has seen that event, for at most five seconds. Ask for the available books after acquiring book `43`, whose event has the ID `2`:

```shell
curl \
  -i \
  -H "Wait-For-Revision: 2" \
  "http://localhost:8080/api/books?available=true"
```

The response contains book `43`, which is available, but not book `42`, which is borrowed:

```
HTTP/1.1 200 OK
Cache-Control: no-cache
Content-Type: application/json
Etag: "2-2b3dd5a380811e1a"
X-Revision: 2
Date: Tue, 29 Sep 2026 21:58:20 GMT
Content-Length: 115

[{"id":"43","title":"2001 – A Space Odyssey","author":"Arthur C. Clarke","isBorrowed":false,"borrowedUntil":""}]
```

The **`X-Revision`** header tells which revision the answer shows. For how the `ETag` header saves a caller from downloading an unchanged answer again, see **[Reading Your Own Writes over HTTP](/docs/architecturekit/reading-your-own-writes-over-http)**.

## Your Turn

Try a few more requests, and use `Wait-For-Revision` to see their effect on the catalog:

- **Return book `42`**, and then lend it to reader `17`.
- **Return book `43`**, although it is not borrowed. The API answers with `200 OK`, but `eventIds` is empty, since there was nothing to do.
- Send a request **without the `content-type` header**, or with a field the request type does not know, and look at the status codes.
