Write a REST interface like Flask


#1

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?


Julia to other language communication
#2

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.


#3

@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.


#4

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.


#5

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()

#6

@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.


#7

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


#8

Let me try


#9

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!")

#10

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

#11

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/
=#

#12

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?


#13

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)


#14

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.


#15

Awesome :smiley:


#16

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.


#17

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


#18

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.


#19

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


#20

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