Variant 2 — drogonR Native API (dr_app / dr_get / …)

The native API is what you use when you’re writing a new HTTP service in R and want full control over routes, request parsing, response shape, and middleware. Each request runs an R closure on the main R thread; the I/O loop, parsing, and connection management stay in C++.

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


Building an app

dr_app() returns a fresh drogon_app (a mutable environment). Routes are added with dr_get() / dr_post() / dr_put() / dr_delete(); each takes the app, a path pattern, and a handler. The helpers return the app, so they pipe.

library(drogonR)

app <- dr_app() |>
  dr_get   ("/health",                function(req) "ok") |>
  dr_get   ("/users/:id",             function(req) {
    dr_json(list(id = req$params[["id"]]))
  }) |>
  dr_post  ("/users",                 function(req) {
    body <- dr_body(req, as = "json")
    dr_json(list(created = body$name), status = 201L)
  }) |>
  dr_delete("/users/:id",             function(req) {
    dr_response(status = 204L)
  })

Path placeholders accept three syntaxes — :id, <id>, {id} — all interchangeable. Captured values arrive in req$params keyed by name. Duplicate (method, path) registrations warn and overwrite the previous handler.


The req object

A handler is called with one argument: a drogon_request. It exposes fields directly and via small accessor helpers.

Field Type Notes
req$method character(1) "GET" / "POST" / …
req$path character(1) URL path with no query string
req$body character(1) raw body as text (UTF-8); use dr_body() to decode
req$headers named character Drogon lowercases names
req$query named character URL-decoded
req$params named list path placeholder captures

Helpers:


Building responses

A handler may return:

# Plain text — the bare-string shorthand.
function(req) "pong"

# JSON. auto_unbox = TRUE turns length-1 R vectors into JSON scalars
# (so list(ok = TRUE) emits {"ok":true}, not {"ok":[true]}).
function(req) dr_json(list(ok = TRUE))

# Explicit status / headers.
function(req) dr_response(
  body    = '{"reason":"gone"}',
  status  = 410L,
  headers = list("Content-Type" = "application/json"))

# Other helpers:
#   dr_text(...)      — status / custom text headers
#   dr_html(...)      — text/html
#   dr_redirect(loc)  — 302 with a Location header
#   dr_file(path)     — stream a file with auto content-type

Throwing an R error from a handler is allowed: the bridge catches it and returns a 500 with a generic body. To customise that body — log the error, render a JSON error envelope, etc. — register an error handler:

app <- dr_app() |>
  dr_on_error(function(req, err) {
    dr_json(list(error = conditionMessage(err),
                 path  = req$path),
            status = 500L)
  }) |>
  dr_get("/risky", function(req) stop("nope"))

Middleware

dr_use(app, mw) appends a middleware to a chain that runs in registration order before the matched route handler. Each middleware takes (req, nxt): call nxt() to delegate downstream (its return value is the response from the next link), or return your own response to short-circuit.

log_requests <- function(req, nxt) {
  t0  <- Sys.time()
  res <- nxt()
  message(sprintf("%s %s -> %s in %s",
                  req$method, req$path, res$status,
                  format(Sys.time() - t0)))
  res
}

require_auth <- function(req, nxt) {
  if (!identical(dr_header(req, "X-Token"), Sys.getenv("APP_TOKEN"))) {
    return(dr_response(status = 401L, body = "unauthorized"))
  }
  nxt()
}

app <- dr_app() |>
  dr_use(log_requests) |>
  dr_use(require_auth) |>
  dr_get("/secret", function(req) "shh")

nxt()’s return is always normalised to a list with status, body, headers, so middleware can mutate it (e.g. res$headers[["X-Tag"]] <- "y"; res) without checking shape.


Static files

dr_static(app, mount, dir) mounts a directory. Files under it are streamed by Drogon directly from a C++ I/O thread — R is never invoked, Range requests work, content-types are auto-detected, and path traversal (.., absolute paths) is rejected with 403.

app <- dr_app() |>
  dr_static("/assets", "./public") |>
  dr_get   ("/api/ping", function(req) "pong")

Starting and stopping the server

dr_serve(app,
         port    = 8080L,
         threads = 4L,        # I/O worker threads inside Drogon
         workers = 1L)        # forked R worker processes; 1 = in-process

# Drive later's loop on the main thread so handlers actually fire.
repeat later::run_now(timeoutSecs = 3600)

dr_serve() returns immediately after Drogon’s I/O threads start. The later::run_now() loop is what dispatches queued requests onto the main R thread; without it, requests pile up and never run. To stop from another R session: dr_stop().

A few rules:


When to reach for the other variants