Write a REST interface like Flask

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

hi, this is another example using Bukdu v0.4.1 (tagged today)

# Bukdu v0.4.1
using Bukdu

struct RESTController <: ApplicationController
    conn::Conn
end

function create(c::RESTController)
    @info :payload (c.params.message, c.params.x, c.params.y)
    render(JSON, "OK")
end

routes() do
    post("/messages", RESTController, create)
    plug(Plug.Parsers, parsers=[:json])
end

Bukdu.start(8080)

#=
curl -H "Content-Type: application/json" http://127.0.0.1:8080/messages -d '{"message": "Hello Data"}'
curl -H "Content-Type: application/json" http://127.0.0.1:8080/messages -d '{"x": 1.2, "y": 2.3}'
=#
3 Likes

I tried to open some informative issues! :slight_smile:

1 Like

Awesome @Seanny123, thank you very much!

2 Likes