# 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/go/making-decisions)** know nothing about HTTP. To accept them over HTTP, define a **request type** for each command, with JSON annotations for the body, and a **function** that turns a request into a command.

Create a file `requests.go`:

```go
package main

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

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

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

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

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

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

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

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

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

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

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

type listBooksRequest struct {
  OnlyAvailable bool `json:"onlyAvailable"`
}

func toListBooks(r *http.Request, request listBooksRequest, user httpapi.NoUser) (ListBooks, error) {
  return ListBooks{
    OnlyAvailable: request.OnlyAvailable,
  }, nil
}

func toGetBook(r *http.Request, _ httpapi.NoBody, user httpapi.NoUser) (GetBook, error) {
  return GetBook{
    BookID: r.PathValue("id"),
  }, nil
}
```

Each function receives the HTTP **request**, the **body**, already decoded into the request type, and the **user** who makes the request. The request holds what the body does not, such as the headers, or the values of the path that `toGetBook` reads below. Nor does the library authenticate its users, so the user is always **`httpapi.NoUser`**, and the reader who borrows a book is named in the body instead. For an API with authentication, see **[Setting Up an HTTP API](/docs/architecturekit/go/setting-up-an-http-api)**.

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

- The ID of a book becomes part of a subject, and composing a subject from an empty ID, or from one with a character that a segment of a subject may not contain, such as a slash, a dot, or a space, 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/go/composing-subjects)**).
- The schema of `BookBorrowed`, which ArchitectureKit derives from the struct, only requires the return date to be a string, so EventSourcingDB would store any text as a return date. Checking the date turns a mistake of the caller into a `400 Bad Request`, before anything is stored.

`toListBooks` and `toGetBook` do the same for the queries. A query is asked with the HTTP method **`QUERY`**, which changes nothing, like `GET`, but carries a body, like `POST`, so a query takes its input from the body, as a command does. `toListBooks` takes from its body whether to list only the available books. `toGetBook` needs no body, so its request type is **`httpapi.NoBody`**, and it reads the ID of the book from the path, where the route below names it `{id}`. That ID only looks up an item and never becomes part of a subject, so `toGetBook` does not need to call `Check`.

## Mapping Items to Responses

The items of the catalog carry no JSON annotations, since what a caller sees is decided by a query and its answer, not by the view. So the answer over HTTP maps the items to a **response type** with JSON annotations, the counterpart of the request types above.

Create a file `responses.go`:

```go
package main

import (
  "context"

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

type bookBody struct {
  ID            string `json:"id"`
  Title         string `json:"title"`
  Author        string `json:"author"`
  IsBorrowed    bool   `json:"isBorrowed"`
  BorrowedUntil string `json:"borrowedUntil"`
}

func bookBodyOf(book BookItem) bookBody {
  return bookBody{
    ID:            book.ID,
    Title:         book.Title,
    Author:        book.Author,
    IsBorrowed:    book.IsBorrowed,
    BorrowedUntil: book.BorrowedUntil,
  }
}

func answerListBooks(catalog architecturekit.View[BookItem]) httpapi.Answer[ListBooks, []bookBody] {
  ask := listBooks(catalog)

  return func(ctx context.Context, q ListBooks) ([]bookBody, error) {
    books, err := ask(ctx, q)
    if err != nil {
      return nil, err
    }

    bodies := make([]bookBody, 0, len(books))
    for _, book := range books {
      bodies = append(bodies, bookBodyOf(book))
    }

    return bodies, nil
  }
}

func answerGetBook(catalog architecturekit.KeyedView[string, BookItem]) httpapi.Answer[GetBook, bookBody] {
  ask := getBook(catalog)

  return func(ctx context.Context, q GetBook) (bookBody, error) {
    book, err := ask(ctx, q)
    if err != nil {
      return bookBody{}, err
    }

    return bookBodyOf(book), nil
  }
}
```

`answerListBooks` answers the `ListBooks` query with `listBooks` from **[Defining a View and a Query](/learn/tutorials/first-steps-with-architecturekit/go/defining-a-view-and-a-query)**, and turns every item into a `bookBody` with `bookBodyOf`. `answerGetBook` does the same for a single book with `getBook`, and hands on an error as it is, so that `query.ErrNoItems` still says that the book does not exist. Their type, **`httpapi.Answer`**, is the type of a function that answers a query over HTTP: it receives the query, but neither the request nor the user. Since the answer decides what a caller sees, the catalog can change its items without changing the API. For details, see **[Handling Queries over HTTP](/docs/architecturekit/go/handling-queries-over-http)**.

## Wiring the API

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

```go
package main

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

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

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

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

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

  err = architecturekit.WaitCaughtUp(context.TODO(), run)
  if err != nil {
    log.Fatal(err)
  }

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

  httpapi.Route(api, mux, "POST /api/acquire-book", toAcquireBook, acquireBook)
  httpapi.Route(api, mux, "POST /api/borrow-book", toBorrowBook, borrowBook)
  httpapi.Route(api, mux, "POST /api/return-book", toReturnBook, returnBook)

  httpapi.Query(api, mux, "QUERY /api/books", toListBooks, answerListBooks(catalog),
    httpapi.Revisioned(catalog, httpapi.DefaultWait),
  )
  httpapi.Query(api, mux, "QUERY /api/books/{id}", toGetBook, answerGetBook(catalog),
    httpapi.Revisioned(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 calls **`WaitCaughtUp`** with the run, which returns once the projection has applied all events that were stored when it started, and only then serves requests. If the projection ends before, `WaitCaughtUp` returns why, and the program stops. An application with several projections hands over all of their runs at once.

**`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 queries need it to let callers read their own writes, as described below.

**`NewPublicAPI`** creates an API without authentication. **`Route`** accepts a command: it decodes the body, turns the request into the command with the given function, and executes the command with its decider. **`Query`** answers a query with the given functions, `QUERY /api/books` with `toListBooks` and `answerListBooks`, and `QUERY /api/books/{id}` with `toGetBook` and `answerGetBook`. It accepts only the method `QUERY`, and decodes the body as `Route` does. Its option **`Revisioned`** lets callers read their own writes from the catalog, as described below.

Start the API:

```shell
go run .
```

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

```text
2026/10/07 08:22:33 Listening on http://localhost:8080.
```

Open another terminal and ask for all books. A query is sent with the method `QUERY` and its input as JSON in the body, which for all books is an empty object. The catalog has been built from the events of the previous steps:

```shell
curl \
  -X QUERY \
  -H "content-type: application/json" \
  -d '{}' \
  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 revision it has written, which is the ID of the last written event. Apart from the `Date` header, which shows the current time in this and all following responses, it looks like this:

```http
HTTP/1.1 200 OK
Cache-Control: no-store
Content-Type: application/json
Date: Wed, 07 Oct 2026 08:22:33 GMT
Content-Length: 17

{"revision":"2"}
```

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/go/making-decisions)**, and the API answers with `422 Unprocessable Entity` and the message of the rejection:

```http
HTTP/1.1 422 Unprocessable Entity
Cache-Control: no-store
Content-Type: application/json
Date: Wed, 07 Oct 2026 08:22:33 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
```

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

```http
HTTP/1.1 400 Bad Request
Cache-Control: no-store
Content-Type: application/json
Date: Wed, 07 Oct 2026 08:22:33 GMT
Content-Length: 81

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

Every error maps to the status code that matches it. For the complete list, see **[Mapping Errors to Status Codes](/docs/architecturekit/go/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 revision from the answer 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 \
  -X QUERY \
  -H "content-type: application/json" \
  -H "Wait-For-Revision: 2" \
  -d '{"onlyAvailable":true}' \
  http://localhost:8080/api/books
```

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

```http
HTTP/1.1 200 OK
Cache-Control: private, no-cache
Content-Type: application/json
Etag: "2-4142521d03aa8052"
Revision: 2
Date: Wed, 07 Oct 2026 08:22:33 GMT
Content-Length: 115

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

The **`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/go/reading-your-own-writes-over-http)**.

## Getting a Single Book

To get a single book, put its ID into the path. The query has no other input, so the request has no body. Ask for book `43`:

```shell
curl \
  -i \
  -X QUERY \
  http://localhost:8080/api/books/43
```

The response contains the book as a single object, rather than as a list:

```http
HTTP/1.1 200 OK
Cache-Control: private, no-cache
Content-Type: application/json
Etag: "2-fe4df14ebdd492c5"
Revision: 2
Date: Wed, 07 Oct 2026 08:22:33 GMT
Content-Length: 113

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

Now ask for a book that does not exist:

```shell
curl \
  -i \
  -X QUERY \
  http://localhost:8080/api/books/99
```

`getBook` returns `query.ErrNoItems`, which the API answers with `404 Not Found` and the message `not found` (see **[Mapping Errors to Status Codes](/docs/architecturekit/go/mapping-errors-to-status-codes)**):

```http
HTTP/1.1 404 Not Found
Cache-Control: no-store
Content-Type: application/json
Date: Wed, 07 Oct 2026 08:22:33 GMT
Content-Length: 24

{"message":"not found"}
```

## 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 the revision is empty, since there was nothing to do.
- Send a request **without the `content-type` header** of the examples, so that `curl -d` sends the body as `application/x-www-form-urlencoded`, or with a field the request type does not know, and look at the status codes.
