Variant 3 — Plumber Drop-In (drogonR::pr_run)

If you already have a plumber service, the shim lets you run it under drogonR by changing one line. The shim parses the plumber router into drogonR routes and dispatches them through dr_serve(). Existing handlers, paths, and parameter types keep working.

For the overall picture see vignette("drogonR", package = "drogonR").


The one-line swap

Existing plumber code:

library(plumber)
pr <- pr() |>
  pr_get ("/users/<id:int>", function(id) list(id = id, ok = TRUE)) |>
  pr_post("/users",          function(req) {
    body <- jsonlite::fromJSON(req$postBody)
    list(created = body$name)
  })

plumber::pr_run(pr, port = 8080L, docs = FALSE)   # <-- before

Becomes:

drogonR::pr_run(pr, port = 8080L, docs = FALSE)   # <-- after

That’s the whole change. docs, swagger, swaggerCallback, quiet are silently accepted and ignored (the shim has no swagger surface, so the flags are inapplicable but valid). Other arguments — threads, workers, max_queue — forward to dr_serve().


What the shim supports

  • @get, @post, @put, @delete annotations and the pr_get/post/put/delete() helpers.
  • Path placeholders <name> and <name:type>. Recognised types are int / integer, dbl / double / numeric, bool / logical; anything else passes through as character. Coercion runs only on path parameters — query and body values keep plumber’s untyped shape (string for query, parsed-JSON for body).
  • Handler argument resolution by name: path > query > JSON body, with req injected if the handler declares a req parameter.
  • Plumber 1.x default serialisation: jsonlite::toJSON(auto_unbox = FALSE) for every return value. Bare strings become JSON arrays (["hello"]), exactly as plumber sends them — byte-level parity with plumber::pr_run(). If you’ve already built a JSON string with jsonlite::toJSON(), it’s emitted verbatim.
  • Returning a dr_response() / dr_json() / dr_text() from a handler opts out of the default serializer and is forwarded as-is — useful for incrementally migrating hot endpoints to drogonR’s response shape without leaving the shim.

What the shim rejects

Each of these triggers an explicit error at pr_run() time, before any route is registered, so failure is loud:

  • @filter / pr_filter() — user-defined filters. Rewrite as middleware via dr_use() (see vignette("mode-native")).
  • pr_hook() / @hook — preroute / postroute / postserialize hooks. Same migration path as filters.
  • pr_mount() / sub-routers — composing one router from several. Flatten into a single pr() (or move to native dr_app()).
  • Custom parsers / serialisers — every response goes through the default plumber JSON serializer. Build the response yourself with dr_response(headers = list("Content-Type" = "...")) if you need another format.
  • PlumberResponse / PlumberFile return values — return a list / data.frame for JSON, a string for text, or a dr_response() for full control.
  • The res parameter in handlers — plumber-style mutation of a passed-in res object isn’t supported. The shim warns once per affected route at pr_run() time. Set status / headers via the return value (dr_response(...)).
  • Async handlers, websockets, OpenAPI / swagger assets — out of scope for the shim.

A minimal end-to-end example

library(plumber)
library(drogonR)

pr <- pr() |>
  pr_get ("/health",                function() list(ok = TRUE)) |>
  pr_get ("/users/<id:int>",        function(id) {
    list(id = id, type = typeof(id))     # id arrives as integer
  }) |>
  pr_post("/echo",                  function(req) {
    list(received = jsonlite::fromJSON(req$postBody))
  })

drogonR::pr_run(pr, port = 8080L, docs = FALSE)

Three responses your client will see:

GET /health         -> {"ok":[true]}
GET /users/42       -> {"id":[42],"type":["integer"]}
POST /echo {"a":1}  -> {"received":{"a":[1]}}

The bracketed scalars are plumber’s default serialiser (auto_unbox = FALSE) — preserved on purpose so existing clients don’t break. To get unboxed JSON, return dr_json(x, auto_unbox = TRUE) from the handler; that bypasses the default serializer.


When to migrate to native

The shim is fine for steady-state plumber apps. Reach for the native API (vignette("mode-native")) if you want:

  • per-request middleware (auth, logging, CORS, rate limiting),
  • explicit control over status / headers / content-type without per-handler dr_response() calls,
  • response helpers (dr_text, dr_html, dr_redirect, dr_file),
  • the full request shape (req$params, dr_query(), dr_body()),
  • path placeholders with :id / <id> / {id} syntax (the shim rewrites plumber’s <id:type> form internally).

If your hot endpoint is C/C++-bound (model inference, embeddings), register that endpoint with dr_get_cpp() and leave the rest under the shim — see vignette("mode-cpp-shared", package = "drogonR").