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

# 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:

- `dr_header(req, "Content-Type")` — case-insensitive header lookup (returns `NULL` if absent).
- `dr_query(req)` — full named character vector; or `dr_query(req, "page")` to pull one value (`NULL` if absent).
- `dr_body(req, as = "text" | "json" | "raw")` — `"json"` parses with `jsonlite::fromJSON()`; `"raw"` returns a raw vector.

## Building responses

A handler may return:

- a bare string — sent as `text/plain; charset=utf-8`, status 200;
- a `dr_response()` list — full control over status, body, headers;
- one of the response helpers — convenience wrappers around `dr_response()`.

    # 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:

- One server per R session. `dr_serve()` after `dr_stop()` errors; Drogon’s event loop can’t be restarted in the same process.
- `workers > 1` spawns forked R worker processes that share the listening socket via `SO_REUSEPORT`. The supervising R session becomes a watchdog; you still poll `later::run_now()` on workers.
- `dr_status()` / `dr_running()` report current state.

## When to reach for the other variants

- If your work is already in C/C++ and you can write a handler matching `drogonR.h`, register it with `dr_get_cpp()` and skip the R-thread hop — see `vignette("mode-cpp-shared", package = "drogonR")`.
- If you have an existing plumber app and just want it faster, swap one line — see `vignette("mode-plumber-shim", package = "drogonR")`.