Write a REST interface like Flask

There are a few posts about write a REST interface using Julia wherein a number of libraries are recommended. However, I’m unclear if any of them resemble Flask?

What I appreciate about Flask is how easy it is to define a set of routes and extract variables from them. For example, I can create a route and define argument types/parsing in a few lines:


@app.route("/init/"
           "region/<str:region>/"
           "site/<int:site_id>/"
           "channel/<int:channel_id>/")
def init(region: str, site_id: int, channel_id: int):
    ...

@app.route("/update/"
           "region/<str:region>/"
           "site/<int:site_id>/"
           "channel/<int:channel_id>/")
def update(region: str, site_id: int, channel_id: int):
    ...

From what I understand so far of the available Julia libraries:

  • HTTP.jl is the foundational library, like Python’s requests library, which doesn’t provide any route or argument parsing functionality
  • Pages.jl seems pretty intuitive, but is lacking a routing and argument parsing example
  • Bukdu.jl is a full-on framework which is inspired by Pheonix. Given my lack of familiarity with Pheonix, this seems like overkill for my use-case.
  • Mux.jl (related to JuliaWebApi.jl) seems to provide the ability to parse routes/URLs?

Consequently, am I correct to conclude Mux.jl would be the most familiar to Python users coming from Flask?

1 Like

Not an expert but I believe Genie.jl can be used quite nicely for anything REST like. It a complete MVC framework but you don’t need to implement all of it.

@Seanny123 Yes, that’s quite straightforward with Genie. You can just check the README: https://github.com/essenciary/Genie.jl

You can see here how to create a new app, how to define routes and inline request handlers, as well as start the web server:

Also, look here to see how to define routes with parameters and how to read the params:

That’s pretty much all you need if you decide to simply return JSON from the route handlers. But if you have some larger app, I recommend that you set up a proper MVC structure and use controllers and JSON views, as described here: https://github.com/essenciary/Genie.jl#rendering-json

I hope this helps - if you have other questions, let me know.

3 Likes

To run an actual Genie application, you recommend doing Genie.REPL.new_app("rest_service"). However, this creates a lot of files that I don’t really need for a REST service. I tried reading through the docs to determine what an actual minimal working example would look like, but I’m a bit lost. Is there a more minimal example than new_app("blah")?

I’ll try to figure this out on my own and will try to make a PR for docs or maybe a new_rest("blah") command once I get some understanding.

1 Like

This is the best minimal example I could come up with:

using Genie

import Genie.Router: route, @params
import Base.convert


convert(::Type{Int}, s::SubString{String}) = parse(Int, s)


route("/sum/:x::Int/:y::Int") do
    return @params(:x) + @params(:y)
end

route("/hello") do
    return "Welcome to Genie!"
end

route("/") do
    return "root"
end

Genie.AppServer.startup()

Base.JLOptions().isinteractive == 0 && wait()
3 Likes

@Seanny123 Correct :slight_smile:

This is mine, in 7 lines of code:

using Genie
import Genie.Router: route
import JSON: json

route("/") do
  (:message => "Hi there!") |> json
end

Genie.AppServer.startup() 

I just put it all in a rest.jl file and run it.
You can build from it by adding more routes, parsing params, etc.

1 Like

Can you run this outside of the REPL without the line at the end of my script Base.JLOptions().isinteractive == 0 && wait()?

Let me try

Small correction, this is the right approach (though a bit uglier) as it returns a proper JSON response (Content-Type: application/json):

using Genie
import Genie.Router: route
import Genie.Renderer: respond
import JSON: json

route("/") do
  Dict(:json => json(:message => "Hi there!")) |> respond
end

Genie.AppServer.startup()

I’ll add a bit of syntactic sugar to make it prettier, maybe:

json!(:message => "Hi there!")

It exits when running $ julia rest.jl

It’s because Genie.AppServer.startup() calls @async HTTP.Servers.serve. It should not start the server @async to not exit.

I guess the simplest (though ugliest) is to add this at the end:

while true
  sleep(1_000_000)
end

I’ll take a look - maybe I’ll remove the @async and switch to starting the server with
@async Genie.AppServer.startup()

Or maybe add an async param to startup()?


Update 1

OK, I couldn’t stand that ugliness so I pushed a few enhancements :slight_smile:

Please make sure you update:

pkg> up

Then you can have rest.jl as:

using Genie
import Genie.Router: route
import Genie.Renderer: json!

Genie.config.session_auto_start = false

route("/") do
  (:message => "Hi there!") |> json!
end

Genie.AppServer.startup(async = false)

And you can run it with

$ julia rest.jl
5 Likes

You could try this with Bukdu.jl v0.3.4 (tagged today)

# Bukdu v0.3.4
using Bukdu

struct RestController <: ApplicationController
    conn::Conn
end

function init(c::RestController, region::String, site_id::Int, channel_id::Int)
    render(JSON, (:init, region, site_id, channel_id))
end

function update(c::RestController, region::String, site_id::Int, channel_id::Int)
    render(JSON, (:update, region, site_id, channel_id))
end

function build_params(c::RestController)
    region::String = c.params.region
    site_id::Int, channel_id::Int = parse.(Int, (c.params.site_id, c.params.channel_id))
    (c, region, site_id, channel_id)
end

init(c::RestController) = init(build_params(c)...)
update(c::RestController) = update(build_params(c)...)

routes() do
    get("/init/region/:region/site/:site_id/channel/:channel_id/", RestController, init)
    get("/update/region/:region/site/:site_id/channel/:channel_id/", RestController, update)
end

Bukdu.start(8080)

#=
curl localhost:8080/init/region/west/site/1/channel/2/
curl localhost:8080/update/region/west/site/1/channel/2/
=#
1 Like

Similar to Genie, I have to add Base.JLOptions().isinteractive == 0 && wait() to the end of the script to stop it from terminating. Is there any other way to stop it from terminating?

If you updated to the latest master doing the wait thing should not be necessary anymore. See above, you can pass Genie.AppServer.startup(async = false)

Sorry, I mis-typed. I did update Genie to master and it worked!

I’m trying Bukdu, because I’m currently having trouble using it in Docker, but I’ll open an issue on the repo when I have more details.

Awesome :smiley:

I understand how to pass an @params via the URL, but is it also possible to send a formatted data via a “header”. For example, make a route able to respond to:

curl -H "Content-type: application/json" \
-X POST http://127.0.0.1:5000/messages -d '{"message":"Hello Data"}'

Which I’ve taken from this Flask example.

Can you please explain what you are trying to achieve? That’s a rather long page that you’re linking to…

Sorry for the long page. I was hoping you could just search for the command and read the code, despite a lack of familiarity with Python. This was not very considerate of me. Let me try again.

I would like to be able to send float values to my REST API. I thought I would have to accomplish this by sending a JSON “payload”. In the above URL, this “payload” is {"message":"Hello Data"}, but I would modify it to {"x": 1.2, "y":2.3}.

Alternatively, if there was some way to pass a float via URL, such as http://127.0.0.1/fsum/1.2/2.3 this would also solve my problem.

For sending json this named pipe / socket is a good option: Which is the right way to integrate Julia program with Node.js

Thanks for the details. I did some digging.

Indeed, there is no logic to handle POST requests with Content-type: application/json although it should be straightforward (it should check the header, extract the body of the request and parse it as JSON). I will add this.

For the other issue, passing floats into the URL, it seems you stumbled onto a bug. Normally something like this should have worked:

Base.convert(::Type{Float64}, s::String) = parse(Float64, s)
route("/somefloats/:x::Float64/:y::Float64") do 
    "x+y is $(@params(x) + @params(y))"
end

But it ends up in a 404 error - so the route matching is getting confused by something.

I’m afraid that until I fix this, the only option is to pass the values over GET:

route("/somefloats") do 
    "x+y is $(parse(Float64, @params(:x)) + parse(Float64, @params(:y)))"
end

and request: http://localhost:8000/somefloats?x=2.5&y=4.5

You could also send a payload as text over GET or POST (as application/x-www-form-urlencoded) and parse the text payload as JSON on the server-side.

I hope this helps.

Do you have a few minutes to open issues on Github for these 2 problems please, so I can look into them these days? Thanks

1 Like