howdy/version

Route requests to controllers by API version.

A version group resolves the requested version with one strategy, then matches the request against that version’s controllers. Later versions fall back to earlier ones for any route they do not override, so a new version only declares what changed.

import howdy/version

let api =
  version.new(version.path())
  |> version.default("v1")
  |> version.add("v1", [user_v1.controller(), order.controller()])
  |> version.add("v2", [user_v2.controller()])

howdy.new()
|> howdy.controller(status.controller())
|> howdy.versions(api)
|> howdy.handler

With the path strategy GET /v2/users hits user_v2 and GET /v2/orders falls back to the v1 order controller. GET /users uses the default version. Handlers read the resolved version from ctx.version.

Strategies:

Only one strategy applies per app. Mixing strategies with precedence rules makes requests ambiguous; if you need it, write a custom resolver.

Header, accept and custom strategies answer 400 for an unknown version and, when no default is set, for a missing one. Header and accept also add a vary header to every response from the group so caches keep versions apart. The path strategy answers 404 in both cases, since the path simply does not match anything.

Versions are opaque strings ordered by declaration. Configuration errors such as duplicate version names panic when the app is built.

Types

A set of versions, each with its own controllers.

pub opaque type Group

How the requested version is read from a request.

pub opaque type Resolver

How a group reads the version, as set by its resolver.

pub type Strategy {
  PathStrategy
  HeaderStrategy(name: String)
  AcceptStrategy(vendor: String)
  CustomStrategy
}

Constructors

  • PathStrategy

    path(): the first path segment, as in /v2/users.

  • HeaderStrategy(name: String)

    header(name): a request header. name is lowercase.

  • AcceptStrategy(vendor: String)

    accept(vendor): a vendor media type such as application/vnd.howdy.v2+json.

  • CustomStrategy

    custom(fn): a function that cannot be described.

Values

pub fn accept(vendor: String) -> Resolver

Read the version from a vendor media type in the accept header. accept("vnd.howdy") matches application/vnd.howdy.v2+json.

pub fn add(
  group: Group,
  name: String,
  controllers: List(controller.Builder(Nil)),
) -> Group

Add a version. Declaration order is the fallback order: a version falls back to the ones added before it, newest first. Adding a version twice or with an empty name panics.

pub fn controllers(
  group: Group,
  name: String,
) -> Result(List(controller.Builder(Nil)), Nil)

The controllers that answer for a version, with fallback applied: its own first, then earlier versions’, newest first. The first route that matches a request wins, so a version’s overrides come before what it inherits. Error for a version that was never added.

pub fn custom(
  resolve: fn(request.Request(context.Body)) -> option.Option(
    String,
  ),
) -> Resolver

Read the version with your own function. Return None when the request carries no version.

pub fn default(group: Group, name: String) -> Group

The version used when a request carries none. Must be added with add before the app is built.

pub fn default_version(group: Group) -> option.Option(String)

The version a request without one gets, if the group has a default.

pub fn header(name: String) -> Resolver

Read the version from a request header, such as x-api-version: v2.

pub fn names(group: Group) -> List(String)

The version names, in declaration order.

pub fn new(resolver: Resolver) -> Group

Create a group that resolves versions with resolver. Fallback to earlier versions is on; see no_fallback.

pub fn no_fallback(group: Group) -> Group

Make every version answer only the routes it declares itself.

pub fn path() -> Resolver

Read the version from the first path segment, such as /v2/users.

pub fn strategy(group: Group) -> Strategy
✨ Search Document