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 following statistical analysis module as a starting point:
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
is generated 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:
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:
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:
route("/api/docs", api_docs)
Creating web pages
Create web pages with Julia code.
Working with Genie projects
Working with Genie in an interactive environment can be useful – but usually we want to persist the application and reuse it between sessions. One way to achieve this is to save it as an IJulia notebook and rerun the cells.