Docs / ArchitectureKit 2.0 / Guides / Setting Up an HTTP API

Setting Up an HTTP API

An optional package exposes commands and queries over HTTP.

To expose commands and queries over HTTP, use the httpapi package:

Changed in 2.0

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

Define a type that describes who is making a request, and a function that determines it from the request. Then call the NewAPI function with the store and this function, and create a mux:

type User struct {
  ID          string
  IsLibrarian bool
}

func userFrom(r *http.Request) (User, error) {
  // ...
}

api := httpapi.NewAPI(store, userFrom)
mux := http.NewServeMux()

If the function returns an error, neither a command nor a query is run, and the request is answered with 401 Unauthorized. An error that has a status code of its own keeps it, though (see Mapping Errors to Status Codes), and so do an error of the category ErrPermanent, one of a write whose outcome is unknown, ErrOutcomeUnknown, and one of a view that did not catch up with a write, ErrNotCaughtUp, which are answered with 500 Internal Server Error and the message internal server error, since neither a command nor a query has run. So if the function can not determine the user because the session store is down, for example, it returns an error of the category ErrTransient. The request is then answered with 503 Service Unavailable, and the failure is logged, rather than sending the caller off to sign in again.

Note that to answer an error that has a status code of its own with 401 Unauthorized all the same, the function wraps it with httpapi.ErrUnauthorized itself, for example with fmt.Errorf("%w: %w", httpapi.ErrUnauthorized, err).

Note that calling NewAPI with nil as the function panics. The store may be nil, though, to test the queries alone (see Testing Queries).

For an application without authentication, call the NewPublicAPI function instead. Commands and queries then receive httpapi.NoUser as user:

api := httpapi.NewPublicAPI(store)

Everything that answers through an API logs every error it does not explain to the caller in full, once, with the method and the route of the request: the routes it wires up, and the functions that answer in a handler of your own. A failure of the server is logged at level Error, and a refusal whose details the caller is not told, such as one with 401, 409, or a 404 for a query that found no item, at level Info (see Handling Commands over HTTP). Only a request that was canceled is not logged, since nothing failed. A panic is logged with its value and its stack. By default, they use the default logger of log/slog. To use the logger of your application instead, hand over the WithLogger option, which NewPublicAPI accepts as well:

api := httpapi.NewAPI(store, userFrom, httpapi.WithLogger(logger))

Note that calling WithLogger with nil panics, and so does calling NewAPI or NewPublicAPI with nil as one of the options.

Determining the User#

To determine the user in a handler of your own, call the UserOf function. If the user can not be determined, it returns an error that wraps httpapi.ErrUnauthorized as well as the error of the function, so that errors.Is and errors.As find either. If the error of the function has a status code of its own, though, it returns that error as it is (see above):

mux.HandleFunc("GET /api/me", func(w http.ResponseWriter, r *http.Request) {
  user, err := httpapi.UserOf(r, api)
  if err != nil {
    // ...
  }

  // ...
})