Lecture 25
plumber and plumber2 are R packages that allow you to create RESTful APIs using specially annotated R scripts.
Both packages make use of a Roxygen-like syntax to provide annotations (decorators) to R functions that then become the API endpoints with specific parameter, behaviors, etc.
plumber2 is a complete rewrite of the original package and it provides a more modern and flexible interface, improved performance, and additional features. The APIs between the two packages are not compatible.
A plumber2 API is just an R script that defines one or more of R functions that have specifically formatted comments that define the endpoint behavior.
Annotations are specified using #* comments directly above the function definition. Annotation keywords usign the @ symbol.
For example the script below defines a single endpoint that responds to GET requests at the /hello path and returns the string “hello world” as json.
To run a plumber API you read the script in using the api() function to create a server object.
The API can then be launched using api_run() and stopped using api_stop(). When used interactively (i.e. in RStudio) the API will run in the background and not block your session.
Both the host/ip and port can be configured via api() or api_run().
One important note is that plumber2 is relatively new and does not have full integration into RStudio yet and in most cases the IDE will get confused and assume your script is a plumber API - make sure to check what is actually running!
When defining an endpoint you must specify the HTTP method(s) that the endpoint will respond to. As we just saw this is done using @method keywords in your annotations.
Each of your plumber2 endpoints can support one or more of the following verbs:
@get@post@put@delete@head@anyWhen making requests to an API endpoint, data can be passed in a number of different ways:
Via the path - e.g. GET /user/<id>
via a query string - e.g. GET /user?id=1231
or via the body of the request - e.g. POST or PUT requests
Path parameters are indicated in the method annotation using <> around the parameter name.
Each parameter specified in the path must have a corresponding argument in the function definition.
While not required, it is good practice to include @param annotations for each parameter to document their purpose.
While this does not typically come up, it is helpful to understand how plumber2 handles potentially ambiguous paths.
When multiple endpoints could match a given request path, plumber2 works through endpoints from least to most ambiguous, e.g.
/path/to/something/specific
/path/to/<name>/specific
/path/to/<name>/<setting>
/path/to/something/*
/path/*
Users may also pass in additional arguments using query strings, e.g. ?q=bread&pretty=1 at the end of a URL.
plumber2 will automatically parse these into a named list which can be accessed via the reserved query argument in the endpoint function.
Similar to path parameters additional documentation on query parameters can be provided via @query annotations.
#* Fake search query endpoint ala DuckDuckGo
#*
#* @get /
#*
#* @query q:string The search query
#* @query pretty:int Whether to pretty-print the results (1) or not (0)
#*
function(query) {
paste0("The q parameter is '", query$q %||% "", "'. ",
"The pretty parameter is '", query$pretty %||% 0, "'.")
}Path and query parameters are transmitted as strings by default, if you are expecting a different type you can specify it using a type hint in the @param or @query annotation.
The following types are supported by default:
| R Type | Plumber Name |
|---|---|
logical |
boolean |
numeric |
number |
integer |
integer |
character |
string |
Date |
date |
POSIXlt |
date-time |
raw |
byte, binary |
| vector | [...] |
list |
object ({name:Type, ...}) |
Both path and query parameters are passed in via the URL - this works for relatively small amounts of data but is not a good option for larger payloads.
Larger data can be passed in the body of the request - this is typically used with PUT, POST, and PATCH requests. The size limit varies a lot depending on the API server configuration but is typically around ~1-10MB.
By default plumber2 will attempt to parse the body based on the Content-Type header of the request. Further control can be achieved using the @parser annotation - there are a large number of built-in parsers available and custom parsers can also be defined.
The result of parsing the body is made available to the endpoint function via the reserved body argument.
By default, the return value of a plumber2 endpoint is serialized to JSON (via jsonlite). This can be overridden by using the @serializer annotation.
Some of the available serializers,
| Annotation | Content Type | Description/References |
|---|---|---|
@serializer html |
text/html; charset=UTF-8 |
Passes response through without |
@serializer json |
application/json |
Object processed with jsonlite::toJSON() |
@serializer csv |
text/csv |
Object processed with readr::format_csv() |
@serializer text |
text/plain |
Text output processed by as.character() |
@serializer jpeg |
image/jpeg |
Images created with jpeg() |
@serializer png |
image/png |
Images created with png() |
@serializer svg |
image/svg |
Images created with svg() |
@serializer pdf |
application/pdf |
PDF File created with pdf() |
@serializer rds |
application/rds |
Object processed with base::serialize() |
@serializer parquet |
application/parquet |
Object processed with nanoparquet::write_parquet() |
#* Plot out data from the palmer penguins dataset
#*
#* @get /plot
#*
#* @query spec:string If provided, filter the data to only this species
#* (e.g. 'Adelie')
#*
#* @serializer png
#*
function(query) {
myData <- penguins
title <- "All Species"
# Filter if the species was specified
if (!is.null(query$spec)){
title <- paste0("Only the '", query$spec, "' Species")
myData <- subset(myData, species == query$spec)
}
plot(
myData$flipper_len,
myData$bill_len,
main=title,
xlab="Flipper Length (mm)",
ylab="Bill Length (mm)"
)
}requestAn additional reserved argument available to plumber2 endpoint functions is request. This is a reqres object that provides access to the full HTTP request details as an R6 object.
From this it is possible to get access to: cookies, headers, raw query string, raw body content, and much more.
response & serverSimilar to request, the response reserved argument provides access to a reqres response object that represents the HTTP response.
Normally, plumber2 will construct this object for you, but if necessary you can modify it directly to set custom headers, cookies, status codes, and body content.
Finally, the server reserved argument provides access to the underlying plumber2 API server object itself. This is useful for advanced use cases.
Plumber2 wraps all endpoints to handle errors that occur during execution.
By default these errors will return a generic 500 Internal Server Error response to the user. However, plumber2 (and reqres) provides a number of helper functions to generate more friendly error messages with appropriate HTTP status codes.
Note - As of right now (v0.1.0) this latter example does not seem to be working as expected - this is likely to be fixed in a future release.
Sta 523 - Fall 2025