Skip to main content

Appendix C — Plumber vs. Plumber2 Syntax

Published

2026-11-06

Why plumber2 exists

plumber and plumber2 both turn R functions into web APIs, and both define routes with #* annotation comments. plumber2 changes what the handler receives (a parsed body and query instead of req), renames the response object, and adds api(), api_get(), and api_post() for building routers in code.

Comparing Syntax

Defining routes

Plumber

In plumber, annotations define the HTTP method, path, and parameters, and each parameter arrives as a function argument:

#* @get /hello
#* @param name The name to greet
function(name = "World") {
  list(message = paste("Hello", name))
}

Plumber2

plumber2 keeps the annotations. Query parameters are declared with @query (with a type) and arrive together in the query argument:

#* @get /hello
#* @query name:string The name to greet
function(query) {
  list(message = paste("Hello", query$name))
}

Handling request bodies

Plumber

In plumber, the handler reads the request through req and sets status codes on res:

#* @post /predict
function(req, res) {
  data <- req$body
  if (!is.data.frame(data)) {
    res$status <- 400
    return(list(error = "Input must be a data frame"))
  }
  # ... prediction logic
}

Plumber2

plumber2 parses the JSON body before the handler runs and passes it in as body; response replaces res:

#* @post /predict
#* @serializer json
function(body, response) {
  if (!is.data.frame(body)) {
    response$status <- 400L
    return(list(error = "Input must be a data frame"))
  }
  # ... prediction logic
}

Which should you choose?

Feature plumber plumber2
Route definition #* annotations or pr_get()/pr_post() #* annotations or api_get()/api_post()
Router object pr() api()
Request data Arguments and req$body Parsed query and body arguments
Response object res response
Parameter types Documented with @param Declared with @query name:type

Recap

  1. Pick plumber when you’re following the original do4ds text or maintaining an API already written with it. Handlers read the request through req and set status codes on res, and routers are built with pr(), pr_get(), and pr_post(); the plumber chapter builds the penguin API this way.

  2. Pick plumber2 when you’re starting a new API and want the request handled for you. It parses the JSON body before the handler runs, declares typed query parameters with @query name:type, and builds routers with api(), api_get(), and api_post(); the plumber2 chapter rebuilds the same API with this syntax.