howdy/openapi/endpoint

Endpoints are handlers that declare their inputs with schemas, so the OpenAPI document shows exactly what each route reads. Mount them on an ordinary controller with endpoint.get, endpoint.post and the rest.

import howdy/openapi/endpoint.{type Endpoint}
import howdy/openapi/schema

pub fn controller() -> controller.Controller {
  controller.new("user")
  |> endpoint.get("/:id", by_id())
}

fn by_id() -> Endpoint(Nil) {
  use <- endpoint.describe([
    endpoint.summary("Find a user"),
    endpoint.response(200, "The user", user.schema()),
    endpoint.error(404, "No user has this id"),
  ])
  use id <- endpoint.path("id", schema.int())
  use ctx <- endpoint.handle
  user_service.find(id)
  |> service.respond(ctx, schema.to_json(_, user.schema()))
}

Each input runs in order before the handler. A missing or malformed parameter answers 400, as howdy/param and howdy/query do. A body that is not JSON answers 400; one that does not match its schema answers 422 with an error for every field, as howdy/validate does.

Like schema objects, an endpoint is documented by running its inputs with placeholder values. Do the work inside handle, not between the inputs, so documenting an endpoint never runs it.

Types

A piece of documentation for an endpoint. See describe.

pub opaque type Doc

A handler together with the inputs it reads and its documentation. guarded is the controller guard’s result type, as for controllers.

pub opaque type Endpoint(guarded)

Values

pub fn body(
  input: schema.Schema(a),
  next: fn(a) -> Endpoint(guarded),
) -> Endpoint(guarded)

A JSON request body, up to body.default_limit bytes.

pub fn body_with_limit(
  input: schema.Schema(a),
  limit: Int,
  next: fn(a) -> Endpoint(guarded),
) -> Endpoint(guarded)

A JSON request body, up to limit bytes.

pub fn delete(
  controller: controller.Builder(guarded),
  path: String,
  endpoint: Endpoint(guarded),
) -> controller.Builder(guarded)
pub fn deprecated() -> Doc
pub fn describe(
  docs: List(Doc),
  next: fn() -> Endpoint(guarded),
) -> Endpoint(guarded)

Document the endpoint. Usually the first line of it.

pub fn description(text: String) -> Doc

A longer description. OpenAPI allows Markdown here.

pub fn empty_response(status: Int, description: String) -> Doc

A response without a body, such as 204.

pub fn error(status: Int, description: String) -> Doc

An error response with Howdy’s error body, {"error": "..."}, as service.error_response sends. Endpoints with inputs document their 400, and those with a body their 422, without being told.

pub fn get(
  controller: controller.Builder(guarded),
  path: String,
  endpoint: Endpoint(guarded),
) -> controller.Builder(guarded)
pub fn handle(
  handler: fn(context.Context(guarded)) -> response.Response(
    content.Content,
  ),
) -> Endpoint(guarded)

The handler, which runs once every input has been read.

pub fn header(
  name: String,
  input: schema.Schema(a),
  next: fn(a) -> Endpoint(guarded),
) -> Endpoint(guarded)

A required request header.

pub fn operation_id(id: String) -> Doc

The operation’s id, which client generators use as a function name. Without one it is the method and path, with parameters named after by: get_user for GET /user, get_user_by_id for GET /user/:id. Ids must be unique; building a document with a repeated one panics.

pub fn optional_header(
  name: String,
  input: schema.Schema(a),
  next: fn(option.Option(a)) -> Endpoint(guarded),
) -> Endpoint(guarded)

A request header that may be left out.

pub fn optional_query(
  name: String,
  input: schema.Schema(a),
  next: fn(option.Option(a)) -> Endpoint(guarded),
) -> Endpoint(guarded)

A query parameter that may be left out.

pub fn patch(
  controller: controller.Builder(guarded),
  path: String,
  endpoint: Endpoint(guarded),
) -> controller.Builder(guarded)
pub fn path(
  name: String,
  input: schema.Schema(a),
  next: fn(a) -> Endpoint(guarded),
) -> Endpoint(guarded)

A path parameter captured by a :name or *name segment. Parameters in the route that an endpoint does not read are documented as strings.

pub fn post(
  controller: controller.Builder(guarded),
  path: String,
  endpoint: Endpoint(guarded),
) -> controller.Builder(guarded)
pub fn put(
  controller: controller.Builder(guarded),
  path: String,
  endpoint: Endpoint(guarded),
) -> controller.Builder(guarded)
pub fn query(
  name: String,
  input: schema.Schema(a),
  next: fn(a) -> Endpoint(guarded),
) -> Endpoint(guarded)

A required query parameter. With a schema.list it takes every occurrence of the key, and is [] when there are none.

pub fn response(
  status: Int,
  description: String,
  body: schema.Schema(a),
) -> Doc

A response with a JSON body described by body. Pass the same schema to schema.to_json when building the response, so they cannot disagree.

pub fn route(
  controller: controller.Builder(guarded),
  method: http.Method,
  path: String,
  endpoint: Endpoint(guarded),
) -> controller.Builder(guarded)

Add an endpoint to a controller, like controller.route.

pub fn security(scheme: String) -> Doc

Require a security scheme declared on the document, such as one added with openapi.bearer_auth. Documentation only: enforce it with a guard or middleware.

pub fn summary(text: String) -> Doc

A one-line summary, shown next to the route.

pub fn tag(name: String) -> Doc

Group the endpoint under a tag. Without one, an endpoint is tagged with the first fixed segment of its path, such as user for /user/:id.

pub fn wrap(
  endpoint: Endpoint(guarded),
  middleware: fn(
    context.Context(Nil),
    fn(context.Context(Nil)) -> response.Response(content.Content),
  ) -> response.Response(content.Content),
) -> Endpoint(guarded)

Run middleware around one endpoint, like middleware.wrap for a handler. It runs after the controller guard. Middleware sees the request without the guard’s value, as middleware always does, and the endpoint gets the value back with whatever the middleware changed.

✨ Search Document