Handlers

Each handler is a function with the signature f(stream::HTTP.Stream). You read and write from the HTTP stream using Bonsai.read or Bonsai.write along with a wrapper type (Body, Query, Headers, Route and Status) to specify the location. The data type being read/written should be an AbstractDict, NamedTuple or have a StructType defined. Both read and write support variadic arguments e.g fn(stream, args...)

Body

Below is an example of reading JSON from a request, and then writing it back as a response

function get_index(stream)
    payload = Bonsai.read(stream, Body(x=Int, y=Float64, z=String))
    # typeof(payload)
    # NamedTuple{(:x, :y, :z), Tuple{Int64, Float64, String}}
    Bonsai.write(stream, Body(payload))
end

Bonsai.write will attempt to set the correct content-type header for the data, however, this can be changed by defining mime_type.

Bonsai.mime_type(::Type{MyType}) = "text/plain"

The content type is defined for the following types already

  • Union{NamedTuple, AbstractDict} - application/json
  • AbstractString - text/plain
  • AbstractPath - Based on the file extension.

Status Codes

The default status code is 200, however other status codes can be returned by using Status

function post_index(stream)
    #...first part of handler
    Bonsai.write(stream, Body("Created"), Status(201))
end

Routing

Routing relies on the router from HTTP.jl, as such the functionality is the same (see the copied doc stings below).

The following path types are allowed for matching:

  • /api/widgets - exact match of static strings
  • /api/*/owner - single * to wildcard match any string for a single segment
  • /api/widget/{id} - Define a path variable id that matches any value provided for this segment; path variables are available in the request context like req.context[:params]["id"]
  • /api/widget/{id:[0-9]+} - Define a path variable id that only matches integers for this segment
  • /api/**- double wildcard matches any number of trailing segments in the request path; must be the last segment in the path

To register a route use the dot syntax to specify the HTTP Method and then dictionary indexing for the requested route. e.g

app.post["/pet/{id}"]

You can extract the route parameters using the Route type, which will return a named tuple.


function get_by_id(stream)
    (;id) = Bonsai.read(stream, Route(id=String))
end

app.get["/{id}"] = get_by_id

The Route constructor takes the parameter name and the type. Multiple keyword arguments are supported

Query

Following a similar pattern as the others, query parameters can be matched by using Query type.


function get_car(stream)
    (;color) = Bonsai.read(
        stream, 
        #/<some route>?color=blue
        Query(color = Union{Nothing, String}),
    )
end

Note to handle optional parameters you can use a union with nothing e.g Union{Nothing, T}.

Headers

Use Headers to read and write specific headers. For example, a handler that can return JSON or a CSV depending on the content type header.


function index(stream)
    headers = Bonsai.read(stream, Headers(content_type=String))
    if headers.content_type == "application/json"
        Bonsai.write(
            stream, 
            Body(json_data),
            Headers(content_type="application/json")
        )
    else
        Bonsai.write(
            stream, 
            Body(csv_data),
            Headers(content_type="text/csv")
        )
    end
end

For Headers the keys will be transformed using Bonsai.headerize and the matching is case-insensitive.

Bonsai.headerize(:content_type)
# "content-type"

Files

Writing files supports AbstractPaths defined in FilePaths. The content type will be set based on the file extension.

file =  Path("data/some-file.json")
Bonsai.write(stream, Body(file))

A nice feature of this is we can easily use other AbstractPath implementations for example like that in AWSS3

file = S3Path("s3://my.bucket/test1.txt") 
Bonsai.write(stream, Body(file))

JSON

Because working with JSON is so common there is a @data macro that can help you define structs for it. This macro supports Base.@kwdef and @composite, for example.

using Dates, UUIDs, Bonsai, StructTypes
using Bonsai: @data

@data struct BaseModel
    id::UUID = uuid4()
    created::Date = now()
    updated::Date = now()
end


"""
Markdown documentation included in OpenAPI description

* name - first and last name
"""
@data struct Person
    BaseModel...
    name::String
end

This expands to roughly the following source code


Base.@kwdef struct Person
    # Note splatted fields from BaseModel 
    id::UUID = uuid4()
    created::Date = now()
    updated::Date = now()
    name::String 
end

# Allows struct to be correctly read from JSON
StructTypes.StructType(::Type{Person}) = StructTypes.Struct()
# Set the correct description to be used in OpenAPI docs
Bonsai.description(t::Type{Person}) = Bonsai.docstr(t)

To help with generating boilerplate you can use JSON3.generatedtypes.