Building REST APIs with plumber
Plumber transforms your R functions into REST API endpoints with minimal code. You add special comments (decorators) above your functions, and plumber handles the HTTP server, request parsing, and response serialization. This makes it easy to expose R computations to web applications, other services, or external systems.
Installation
Install plumber from CRAN:
install.packages("plumber")
library(plumber)
Plumber works with R 3.6 and later. It bundles a web server (based on httpuv), so you do not need to install additional server software.
Your First API
Create a file named plumber.R:
#* Echo back the input message
#* @param msg The message to echo
#* @get /echo
function(msg = "") {
list(
msg = paste0("You sent: '", msg, "'")
)
}
#* Return a histogram plot
#* @get /plot
#* @serializer png
function() {
hist(rnorm(100), main = "Random Normal Distribution")
}
Start the API with:
pr("plumber.R") |>
pr_run(port = 8000)
Visit http://localhost:8000/echo?msg=hello to see the echo endpoint. The ?msg=hello query parameter maps to the msg function argument. Visit http://localhost:8000/plot to see the PNG histogram.
HTTP Methods
Plumber supports all common HTTP verbs:
#* Get a resource
#* @get /users
#* Create a new resource
#* @post /users
#* Update an existing resource
#* @put /users/:id
#* Delete a resource
#* @delete /users/:id
#* Partial update
#* @patch /users/:id
The @use decorator handles all methods on a route. Each method maps to the corresponding HTTP request type.
Path Parameters
Capture dynamic segments in URLs using :paramname:
#* Get user by ID
#* @get /users/:id
function(id) {
# id is a character string from the URL
user <- get_user_by_id(as.integer(id))
list(user = user)
}
#* Calculate stats between two values
#* @get /range/:min/:max
function(min, max) {
values <- seq(as.numeric(min), as.numeric(max), length.out = 100)
list(
min = min(values),
max = max(values),
mean = mean(values),
sd = sd(values)
)
}
Request http://localhost:8000/users/42 and plumber passes “42” as the id argument.
Query Strings and Request Bodies
Query parameters become function arguments automatically:
#* Filter data
#* @get /data
function(species = NULL, limit = 10) {
data <- iris
if (!is.null(species)) {
data <- subset(data, Species == species)
}
head(data, limit)
}
Access request bodies with req$body:
#* Create a new record
#* @post /records
#* @parser json
function(req, res) {
body <- req$body
# body is now a list parsed from JSON
new_id <- insert_record(body)
list(
success = TRUE,
id = new_id
)
}
The @parser json decorator tells plumber to parse JSON request bodies. Without it, plumber defaults to form parsing.
Returning JSON
Plumber automatically serializes lists to JSON. For other formats, use serializers:
#* Return JSON (default)
#* @get /json
#* @serializer json
function() {
list(status = "ok", data = 1:10)
}
#* Return plain text
#* @get /text
#* @serializer text
function() {
"Plain text response"
}
#* Return HTML
#* @get /html
#* @serializer html
function() {
"<html><h1>Hello World</h1></html>"
}
#* Return CSV
#* @get /csv
#* @serializer csv
function() {
iris[1:5, ]
}
Common serializers include: json, csv, png, jpeg, html, text, and contentType for custom MIME types.
Error Handling
Throw errors with stop() or use the response object:
#* Get user or return 404
#* @get /users/:id
function(id, res) {
user <- tryCatch(
get_user_by_id(as.integer(id)),
error = function(e) NULL
)
if (is.null(user)) {
res$status <- 404
return(list(error = "User not found"))
}
list(user = user)
}
You can also create a filter for global error handling:
#* Log errors and return clean messages
#* @filter error_log
function(req, res) {
tryCatch(
forward(),
error = function(e) {
message("Error: ", e$message)
res$status <- 500
list(error = "Internal server error")
}
)
}
Apply this filter to all endpoints by placing it in your plumber.R file. Use @preempt filter_name to skip a filter for specific endpoints.
Filters and Middleware
Filters run before endpoints and can modify requests or responses:
#* Add CORS headers
#* @filter cors
function(req, res) {
res$setHeader("Access-Control-Allow-Origin", "*")
res$setHeader("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE")
forward()
}
#* Require authentication
#* @filter auth
function(req, res) {
token <- req$headers[["Authorization"]]
if (is.null(token) || !valid_token(token)) {
res$status <- 401
return(list(error = "Unauthorized"))
}
forward()
}
#* This endpoint requires auth
#* @preempt auth
#* @get /protected
function() {
list(data = "Sensitive information")
}
Programmatic API Definition
For complex APIs, build routers programmatically:
library(plumber)
pr() |>
pr_get("/hello", function() list(message = "Hello!")) |>
pr_post("/sum", function(a, b) as.numeric(a) + as.numeric(b)) |>
pr_run(port = 8000)
This “tidy” interface makes it easier to conditionally add endpoints or build APIs from multiple files.
Deployment
Run plumber in production with a process manager or container:
# Run as a background process on a specific port
plumb("plumber.R") |>
pr_run(host = "0.0.0.0", port = Sys.getenv("PORT", 8000))
Common deployment options:
- Posit Connect: Push-button deployment from RStudio
- Docker: Package R + plumber in a container
- Systemd: Run as a Linux service
- Cloud platforms: Render, Heroku, AWS ECS
For Docker, create a app.R file:
library(plumber)
plumb("plumber.R") |>
pr_run(host = "0.0.0.0", port = as.integer(Sys.getenv("PORT", 8000)))
Then build and run:
FROM r-base:latest
RUN R -e "install.packages('plumber')"
COPY app.R plumber.R .
EXPOSE 8000
CMD ["R", "-e", "source('app.R')"]
See Also
- HTTP Requests with httr2 — Make HTTP requests from R to consume external APIs
- Building Shiny Apps — Create interactive web applications with Shiny
- Debugging R Code — Fix errors in your R applications
Plumber is the standard way to expose R computations as web services. Start simple with one endpoint, then add routing, authentication, and filters as your API grows.