Revise
-compatible hot-reloading middleware for web development in Julia
This package provides a collection of utilities and HTTP middleware that improves developer experience while working on server-based Julia projects.
The following modules are exported and make up the public API of this package.
Provides the dev
and prod
function. These start up a "development" and
"production" server respectively. The dev
server includes features such as:
Automatic page updates on file changes without refreshing browser tabs. This
will watch all source files for changes and push the updated HTML to the
browser via SSE (server sent events). New HTML content is swapped with the
current HTML with idiomorph
.
This feature integrates with Revise
and a router auto-reloader, which allows
for addition and removal of route definitions in the server without the need to
restart it. The usual limitations of Revise
apply to this feature, ie. no
struct
re-definitions, etc.
When starting up a dev
server a browser tab is opened pointing to the /
route. On macOS if using a Chromium-based browser then if there is alread a tab
open pointing at this server then it is reused. This uses the same approach
used by Vite.
"DOM-to-Source" lookup, via HypertextTemplates.jl
. When using this package
for template rendering source information for all nodes in the rendered web
page are stored and can be used to jump to the source location in your default
editor by selecting a part of the webpage, typically some text, and pressing
Ctrl+2
. Pressing Ctrl+1
will jump to the root @render
macro call that
generated the element, which can be useful if the current state of a page
consists of HTML generated by different routes. This feature requires Revise
to be imported for it to function.
All documentation for defined routes that make up the application is available
at the /docs/
route. This change be changed via the docs
keyword provided
by dev
. This provides a summary overview and details view for each route,
which includes any attached docstrings, path
, query
, and body
types,
along with source code links that will open your default editor to the source
of the route.
Any errors that are thrown during development cause a separate browser tab to open with an interactive view of the stacktrace and error message. Source links in the stacktrace are clickable and open your default editor at that location.
The /errors/
root route itself lists all errors that the application has
encountered while running, and allows you to navigate back to previous errors.
The name of this route change be changed if it conflicts with your application
by changing the errors
keyword in the dev
call.
The prod
function removes all the above features of dev
. Aside from that
the interface is identical. Ensure that Revise
is not loaded when running a
server via prod
in production since you may leak source location information
from rendered templates.
This module provides the route macros @GET
, @POST
, @PUT
, @PATCH
, and
@DELETE
matching their HTTP verbs, along with @STREAM
and @WEBSOCKET
macros. These are used to define HTTP
route handlers within the enclosing
module
. They all follow the pattern below:
module Routes
using ReloadableMiddleware.Router
# Handler functions must be unnamed function definitions.
@GET "/" function (req)
# Anything can be returned from the handler. It will be converted
# to an `HTTP.Response` automatically. See the `Responses` section
# below for details.
return "page content..."
end
# Use the same `{}` syntax as `HTTP`. Additionally include a `path` keyword
# that declares the type of each path parameter. `StructTypes` is used for
# the deserialization of these parameters.
@GET "/page/{id}" function (req; path::@NamedTuple{id::Int})
@show path.id
# ...
end
# Post requests specify their typed body contents as below. This is
# expected to be urlencoded `Content-Type`.
@POST "/form" function (req; body::@NamedTuple{a::String,b::Base.UUID})
@show body.a, body.b
# ...
end
# Query parameters are specified using the `query` keyword. These are
# urlencoded values, and are deserialized, like the previous examples
# using `StructTypes`.
@GET "/search" function (req; query::@NamedTuple{q::String})
@show query.q
# ...
end
# JSON data can be deserialized using the `JSON` type. Anything that
# the `JSON3` package handles can be handled with this type. `allow_inf`
# is set to `true` for deserialization.
@POST "/api/v1/info" function (req; body::JSON{Vector{Float64}})
@show vec = body.json
# ...
end
# Multipart form data can be deserialized using the `Multipart` and `RawFile`
# type as below.
@POST "/file-upload" function (req; body::Multipart{@NamedTuple{file::RawFile}})
@show body.multipart.file body.multipart.data
# ...
end
# Multipart form data can be deserialized using the `Multipart` and `File`
# type as below. `.data` will contain a `Vector{Int}` rather than raw bytes.
# The `Content-Type` of the part denotes how it will be deserialized, either
# as JSON or urlencoded.
@POST "/file-upload-typed" function (req; body::Multipart{@NamedTuple{file::File{Vector{Int}}}})
@show body.multipart.file body.multipart.data
# ...
end
The @prefix
and @middleware
macros allow for defining "scoped" routes. This
provides a way to group a set of routes under a common path prefix and a scoped
set of middleware that should be applied in addition to the Server
-level
middleware.
These macros work on the module-level, ie. only a single usage per-module is
allowed and affects all routes defined within the module. Use different modules
to separate different sets of routes, for example a Routes.API
module to
define a JSON API that lives under a /api/v1
route prefix.
module Routes
@GET "/" function (req)
# The actual `/` handler.
end
module API
@prefix "/api/v1"
@middleware function (handler)
function (req)
# Run custom middleware code here. Only # runs for routes under `/api/v1`.
return handler(req)
end
end
@GET "/" function (req)
# Handles `/api/v1`, not `/`.
end
end
end
You can construct valid paths to any defined route by calling the route handler
function with no positional arguments and either/both of the path
and query
keyword arguments. If called with invalid values it will fail at runtime to
construct the path. This only works for named route handler functions.
@GET "/users/{id}" function user_details(req; path::@NamedTuple{id::Int})
# ...
end
# ...
user_details(; path = (; id = 1)) == "/users/1"
user_details(; path = (; id = "id")) # Throws an error.
This module is responsible for serializing returned values from route handlers
into valid HTTP.Response
s. Any value returned from a handler is supported.
String
s are content-sniffed using HTTP.sniff
to determine Content-Type
.
Base.Docs.HTML
and Base.Docs.Text
are set to text/html
and text/plain
respectively. Simple values such as numbers, symbols, and characters are sent
as text/plain
. Other values are serialized as application/json
using the
JSON3
package, using the allow_inf = true
keyword.
The module exports a response
helper function that allows for declaring the
intended serialization mime type, along with other options such as attachment
and filename
which effect the Content-Disposition
header.
This module offers interfaces that enhance features and behaviors of the
package but that rely on external Julia packages that are not direct
dependencies of ReloadableMiddleware
.
This middleware function provides integration with Bonito.jl
such that users
can embed Bonito.App
s into rendered pages without having to manually setup
WebSocket connections and handle closing unused sessions. Just add
middleware = [bonito_middleware(), ...]
to your Server.prod
and Server.dev
calls that launch your server. Then
embed App
s into the rendered HTML from any routes and they will maintain an
open WebSocket with the backend. This is useful for such things as interactive
WGLMakie
plots.
Note that when a user nagivates away from a page that contains an active
Bonito.App
the connection will be closed and the server will terminate the
Bonito.Session
and all associated assets that were being served for it to
function. If a user then decides to navigate back to that page with their
browser nagivation they may not get a newly requested page content, which
means there is no running Bonito
session to connect to. This is intentional.
Use a Vary: *
header for any pages that contain such dynamic content so that
the browser is forced to do a full refetch of the page content. This will start
up a new Bonito
connection and session.