How to structure a julia package which should also contain a REST api?

So what I want to do: I want to build a julia package Foo.jl which should be used by users writing scripts in julia primarily. There’s also a secondary group of users who will want to use a subset of the package through a REST API.

The most simple structure I can think of is this:

.
├── Manifest-v1.12.toml
├── Project.toml
├── server.jl
└── src
    └── Foo.jl

which I don’t really like since

  1. It limits the server code to a single file
  2. It issues using Foo but foo is just a package that’s not registered. It lives only in the same directory as server.jl.

I don’t want to split this up into several repos though. I want a monorepo design.

I could do

.
├── api
│   └── server.jl
├── Manifest-v1.12.toml
├── Project.toml
└── src
    └── Foo.jl

which solves 1. but not 2.

Has anyone a good feel for what the best practice here is?

The server.jl code is something like this:

using Oxygen
using HTTP
using Foo # The package

@get "/greet" function (req::HTTP.Request)
    return "hello world!"
end

# start the web server
serve()

which uses Oxygen.jl but this question is not limited to Oxygen.jl of course.

This sounds mostly like a question of perspective. From my point of view it’s server.jl that happens to live within the Foo package and thus there’s nothing strange about using Foo. But then I would also start server.jl with

using Pkg
Pkg.activate(@__DIR__)
Pkg.instantiate()

On the other hand you’re using Julia 1.12, so the best practice would probably be to use the Pkg workspace feature to set up the api environment and package the REST api as a Pkg App.

3 Likes

That makes sense. I think I will use this approach first then. I’m not familiar with the App concept in Pkg yet.

Hi @DoktorMike,

I’d also heavily recommend using the @oxidise macro in your package.

If your consumers also have Oxygen.jl installed in the same project they’ll end up using the same internal state object - which will cause all routes to get registerd to the same instance.

This macro will create a new internal context object in the current module (and bind all stateful methods to it) instead of leveraging the global one and prevent any accidental collisions or overwrites.

module MyServer

using HTTP
using Oxygen; @oxidize 
# now all the @get, @put, serve()... methods are bound to this module

export start, stop

# if we want to register routes outside of this module and have them bound to the same internal state, you'll need to add the exports to this module like this:
export @get, @post

@get "/" function()
    return text("hello world")
end 

start() = serve()
stop() = terminate()

end

Now somewhere else you can import your sever module and work with it

module Main

# only pull in the text helper method since @get is coming from the custom module
using Oxygen: text

# now we have access to the exported methods of the server
include("./MyServer.jl"); using .MyServer

# And we can register new routes on the same server instance outside of the file
@get "/custom" function()
     return text("a custom endpoint")
end

start()

end
1 Like