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: GitHub - GenieFramework/Genie.jl: 🧞The highly productive Julia web framework
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:
https://github.com/essenciary/Genie.jl#working-with-genie-apps-projects
Also, look here to see how to define routes with parameters and how to read the params:
https://github.com/essenciary/Genie.jl#using-genie-in-an-interactive-environment-jupyterijulia-repl-etc
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
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()
?
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
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.
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.
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