--- title: "Migrating from Shiny (and plumber v1)" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Migrating from Shiny (and plumber v1)} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set(eval = FALSE) ``` 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 (`` in the annotation) become named arguments. Read the query string from the reserved `query` argument and a parsed body from `body`. ```r # 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 (``) | | 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.