Creating an API

REST APIs provide a structured, standardized way to expose and access data and functionalities over the internet, making it easier to integrate different software systems. REST (Representational State Transfer) means that the server does not store any state about the client session. Each request is independent from previous ones.

Creating an API with Genie Framework is as easy as adding a set of endpoints linked to a specific functionality. Moreover, it is possible to automatically generate an API documentation page with Swagger.

API endpoints

An API is comprised of multiple endpoints, each of which requires:

  • A route that defines the URL path, parameters, handler, and HTTP method to access a resource.
route("/api/hello/:name", greet, method=GET)
  • A handler function that processes the request and returns a response
function greet()
    "Hello $(params(:name))"
end

Any information sent to the API is passed either as a parameter in the URL path for a GET request or in the body for a POST request. In the example above we'd pass the name with /api/hello/John. This can be then extracted by the handler with the params call.

Let's use the statistical analysis module from the First multipage app guide:

module StatisticAnalysis
export gen_numbers, calc_mean
function gen_numbers(N::Int)
    return rand(N)
end
function calc_mean(x::Vector{Float64})
    return round(sum(x) / length(x); digits=4)
end
end

Suppose you want to make the gen_numbers function accessible via an API endpoint so that a vector of random numbers of length N at /api/numbers/:N. Create an API module to hold the endpoint handlers, and the route as

module App
using Genie

module StatisticAnalysis
    . . .
end

module API
using Genie.Requests
using Genie.Renderer.Json
using ..StatisticAnalysis
function numbers()
    json(:x => gen_numbers(payload(:N)))
end
end

route("/api/numbers/:N::Int", API.numbers)

end

Note that the response is formatted as a JSON dictionary in order to make it easier to consume by third parties. Moreover, the type of the N parameter in the URL is set to Int so that the payload function will return an Int.

Launch the app, and test it it with

❯ curl localhost:8000/api/numbers/10
{"x":[0.2599349703004966,0.41507586327531465,0.7117052551417936,0.04899936953416706,0.4841727037100556,0.6098044830424467,0.36874657414451883,0.7934650411840747,0.33030836077261927,0.5295794718912344]}%

Accepting JSON payloads

A common requirement when exposing APIs is accepting POST payloads. That is, requests over POST, with a request body as a JSON encoded object. We can build an echo service like this:

using Genie, Genie.Renderer.Json, Genie.Requests
using HTTP

route("/echo", method = POST) do
  message = jsonpayload()
  (:echo => (message["message"] * " ") ^ message["repeat"]) |> json
end

route("/send") do
  response = HTTP.request("POST", "http://localhost:8000/echo", [("Content-Type", "application/json")], """{"message":"hello", "repeat":3}""")

  response.body |> String |> json
end

up(async = false)

Here we define two routes, /send and /echo. The send route makes a HTTP request over POST to /echo, sending a JSON payload with two values, message and repeat. In the /echo route, we grab the JSON payload using the Requests.jsonpayload() function, extract the values from the JSON object, and output the message value repeated for a number of times equal to the repeat value.

If you run the code, the output should be

{
  echo: "hello hello hello "
}

If the payload contains invalid JSON, the jsonpayload will be set to nothing. You can still access the raw payload by using the Requests.rawpayload() function. You can also use rawpayload if for example the type of request/payload is not JSON.

Documenting the API

It is beneficial idea to add a documentation page, making API endpoints discoverable user-friendly. You can use the SwaggUI.jl and SwaggerMarkdown packages to individually document each endpoint and automatically generate a documentation page.

First, add a docstring to a route using the @swagger macro, following the OpenAPI format:

app.jl
using SwagUI, SwaggerMarkdown

@swagger """
/api/predict/{id}:
  get:
    description: Predict the MEDV of a house.
    parameters:
      - in: path
        name: N
        required: true
        description: Length of the random number vector.
        schema:
           type: integer
    responses:
      '200':
        description: OK
"""
route("/numbers/:N::Int", AnalysisController.api_numbers, method=GET)

Second, add a handler that builds the documentation page:

AnalysisController.jl
function api_docs()
    info = Dict{String,Any}()
    info["title"] = "Statistic analysis API"
    info["version"] = "1.0.5"
    openApi = OpenAPI("3.0", info)
    swagger_document = build(openApi)

    render_swagger(swagger_document)
end

Finally, add a route to the documentation page:

app.jl
route("/api/docs", api_docs)