The hardware and bandwidth for this mirror is donated by METANET, the Webhosting and Full Service-Cloud Provider.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]metanet.ch.

Migrating from Shiny (and plumber v1)

Two audiences land here: people coming from Shiny (reactive server) and people porting a plumber v1 API (aurora targets plumber2 only). This covers both shifts.

From Shiny: the mental-model shift

Shiny aurora
Reactive server holds state per user Stateless: state lives in the client or an external store
ui <- fluidPage(...) build_ui() returns an htmltools/bslib tag, compiled to static HTML
server <- function(input, output) {...} routers/*.R — plumber2 handlers returning JSON
reactive() / observe() JS fetch (aurora.json(...)) + DOM updates in app.js
input$x request query / body / path params
output$y <- render*() a JSON response a handler returns
renderPlot/renderDT widgets client-side libraries (ECharts, MapLibre, DataTables) fed by /api
shinyapps.io / Connect, sticky sessions Docker / ShinyProxy, horizontally scalable

What you keep: the bslib UI transfers almost verbatim. What changes: server logic becomes JSON endpoints, and you write some JavaScript to render. What you gain: no per-user R process, trivial horizontal scaling, CDN-cacheable UI.

A reactive value that recomputed when an input changed becomes: a DOM event → aurora.json("api/...") → update the DOM. Read-only datasets that you loaded once at app start map onto aurora_data_store() (see vignette("aurora")).

From plumber v1: it is not a find-and-replace

plumber2 is API-incompatible with plumber. The five changes that actually bite:

1. Query params no longer bind to named handler args

Only path parameters (<var> in the annotation) become named arguments. Read the query string from the reserved query argument and a parsed body from body.

# v1 (BROKEN under plumber2 — msg is always "")
#* @get /api/echo
function(msg = "") list(echo = msg)

# plumber2
#* @get /api/echo
function(query) list(echo = query$msg %||% "")

2. req/res become reqres request/response

The reserved handler arguments are request, response, query, body, server, client_id — not req/res. Translation table:

Need plumber v1 plumber2 / reqres
Path param named arg (:var) named arg (<var>)
Query value named arg query$x
Parsed body req$body$x body$x (needs @parser json)
Request method / path req$REQUEST_METHOD / req$PATH_INFO request$method / request$path
A request header req$HTTP_X_FOO request$get_header("X-Foo")
Cookies parse req$HTTP_COOKIE by hand request$cookies$name (auto-parsed)
Set status res$status <- 401 response$status <- 401L
Set header res$setHeader(n, v) response$set_header(n, v)
Set / clear cookie manual Set-Cookie response$set_cookie(...) / response$clear_cookie()
Abort with a code res$status <- n; return(...) reqres::abort_unauthorized() / abort_bad_request()
Continue / stop chain forward() / return() return plumber2::Next / plumber2::Break
Logging cat() server$log("message", ...)

Note: reqres set_cookie(same_site=) wants "Lax"/"Strict"/"None" (capitalised). length-1 vectors are not auto-unboxed by the json serializer, so scalars serialize as 1-element arrays — jsonlite::unbox() them where a scalar is required (or use a dedicated serializer like geojson).

3. No @filter / preempt / forward()

Removed. Use a route chain instead: a header-route handler (@header) runs before the body and can reject early; return Next to continue or Break to stop; throw reqres::abort_*() to fail with a status. aurora’s auth template uses exactly this for its /api/* guard (see vignette("auth")).

4. pr_*()api_*() (not 1:1)

pr()/pr_mount()api() + api_parse(); pr_static()api_assets(); pr_hook("exit", ...)api_on("end", ...). aurora already does this assembly for you in aurora_app().

5. No mount-prefixing — the path lives in the annotation

v1 mounted a router under a prefix; aurora bakes the full path into the annotation (#* @get /api/iniciativas/data). aurora_add_route() writes it for you; porting a mounted v1 router means rewriting each annotation to its full path.

Testing a ported handler

pa$test_request(fiery::fake_request(url, method=, content=, headers=)) runs a request through the assembled API without binding a port — handy for fast, deterministic checks while you port.

These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.