Introduction to Plumber and REST
Understand REST principles and annotate R functions as API endpoints.
Introduction to Plumber and REST is a free R Academy lesson on CoddyKit — lesson 1 of 4. You can read the complete lesson below for free — then practise it hands-on in the browser with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, one of 4 lessons in the course, and your progress syncs across the web and the CoddyKit app.
What Is a REST API?
A REST (Representational State Transfer) API is a web service that exposes data and operations via HTTP. Key principles:
- Stateless: each request contains all information needed; no server-side session.
- Resource-oriented: endpoints represent resources (
/users,/predictions). - Standard HTTP verbs: GET (read), POST (create), PUT (update), DELETE (remove).
- JSON: the standard data format for request and response bodies.
# REST API concepts in HTTP terms:
# GET /api/model/predict?x=5 -> read a prediction
# POST /api/model/train -> create a new model
# GET /api/data/summary -> read data summary
# DELETE /api/cache/flush -> remove cached results
# Plumber maps R functions to these HTTP endpoints
cat('REST: stateless, resource-oriented, JSON responses')plumber Annotation Syntax
plumber uses special comment annotations starting with #* to define API endpoints. Place an annotation directly above the R function that handles the endpoint. The function arguments map to request parameters; the return value becomes the JSON response body.
# plumber.R
library(plumber)
#* @get /ping
function() {
list(status = 'ok', time = Sys.time())
}
#* @get /add
#* @param a:int First number
#* @param b:int Second number
function(a, b) {
list(result = as.integer(a) + as.integer(b))
}pr() — Creating a Plumber Router
pr('plumber.R') reads a plumber file and creates a router object that registers all annotated endpoints. The router is the central object you configure (add filters, serialisers, etc.) before running.
library(plumber)
# Create a router from a plumber file
api <- pr('plumber.R')
# Inspect registered routes
print(api$routes)
# Alternatively, define inline without a file:
api <- pr() |>
pr_get('/ping', function() list(status = 'ok')) |>
pr_post('/echo', function(req) req$body)pr_run() — Starting the Server
pr_run(router, host, port) starts the plumber API server. By default it binds to 127.0.0.1:8000. Set host = '0.0.0.0' to accept connections from any network interface (required for Docker or remote access).
library(plumber)
api <- pr('plumber.R')
# Start server on localhost port 8000
# pr_run(api, host = '127.0.0.1', port = 8000)
# For Docker/remote access, bind to all interfaces
# pr_run(api, host = '0.0.0.0', port = 8000)
# View auto-generated Swagger docs in browser
# (automatically available at /docs or /__docs__/ endpoint)
cat('Swagger UI auto-generated at http://localhost:8000/__docs__/')@get Annotation
The #* @get /path annotation maps a GET request to the function. Query string parameters (e.g. ?name=Alice) are automatically passed as R function arguments. If no conversion annotation is given, parameters arrive as character strings.
# plumber.R
#* Greet a user by name
#* @param name:str The name to greet
#* @get /greet
function(name = 'World') {
list(
message = paste('Hello,', name),
timestamp = format(Sys.time(), '%Y-%m-%d %H:%M:%S')
)
}
# GET /greet?name=Alice
# -> {"message":"Hello, Alice","timestamp":"2026-01-01 12:00:00"}@post Annotation
The #* @post /path annotation maps a POST request to the function. The request body (typically JSON) is accessible via the special req argument as req$body (a parsed list when the request body is JSON). POST is used for operations that create resources or trigger computations.
# plumber.R
#* Run a linear model prediction
#* @post /predict
function(req) {
# req$body is already parsed from JSON
input_data <- as.data.frame(req$body)
# Run prediction with a pre-loaded model
predictions <- predict(trained_model, newdata = input_data)
list(
predictions = as.numeric(predictions),
n = nrow(input_data)
)
}JSON Serializer
By default, plumber serialises return values to JSON using jsonlite. The #* @serializer json annotation makes this explicit. You can configure serialiser options like pretty-printing or null handling by specifying them as a JSON list in the annotation.
# Default: automatic JSON serialization
#* @get /data
function() {
list(values = 1:5, labels = c('a', 'b', 'c', 'd', 'e'))
}
# Explicit JSON serializer with options
#* @serializer json list(na = 'null', auto_unbox = TRUE)
#* @get /data_explicit
function() {
list(value = 42, missing = NA)
}
# With auto_unbox=TRUE: {"value":42} not {"value":[42]}HTTP Verbs — PUT, DELETE, PATCH
plumber supports all standard HTTP verbs via matching annotations:
#* @put /path: full replacement of a resource.#* @delete /path: remove a resource.#* @patch /path: partial update of a resource.#* @head /path: headers only (no body).
# plumber.R — CRUD-style endpoints
#* Update a model configuration
#* @put /config/<model_id>
function(model_id, req) {
config <- req$body
save_config(model_id, config)
list(updated = model_id, config = config)
}
#* Remove cached results
#* @delete /cache/<key>
function(key) {
cache_env <- globalenv()$cache
rm(list = key, envir = cache_env)
list(deleted = key)
}Path Parameters
Path parameters are defined using angle brackets in the route: /user/. plumber extracts the value from the URL and passes it as a function argument with the same name. These are different from query parameters (which appear after ?).
# plumber.R
#* Get stats for a specific dataset
#* @param dataset_id:str The dataset identifier
#* @get /datasets/<dataset_id>/stats
function(dataset_id) {
if (!dataset_id %in% available_datasets()) {
stop(paste('Dataset not found:', dataset_id))
}
ds <- load_dataset(dataset_id)
list(
id = dataset_id,
rows = nrow(ds),
cols = ncol(ds),
names = names(ds)
)
}Error Handling
When an R function throws an error, plumber catches it and returns a 500 HTTP response with the error message in JSON. For user-facing APIs, return appropriate HTTP status codes explicitly using res$status and stop() for validation errors.
# plumber.R
#* Divide two numbers safely
#* @get /divide
function(a, b, res) {
a <- suppressWarnings(as.numeric(a))
b <- suppressWarnings(as.numeric(b))
if (is.na(a) || is.na(b)) {
res$status <- 400 # Bad Request
return(list(error = 'Both a and b must be numeric'))
}
if (b == 0) {
res$status <- 422 # Unprocessable Entity
return(list(error = 'Division by zero is not allowed'))
}
list(result = a / b)
}Auto-Generated Swagger Documentation
plumber automatically generates interactive Swagger UI documentation from your annotations. Visit /__docs__/ when the server is running to see all endpoints, their parameters, and try them in the browser. Use #* @tag to group endpoints logically.
# plumber.R with Swagger metadata
#* @apiTitle My ML Prediction API
#* @apiDescription Serves predictions from trained R models
#* @apiVersion 1.0.0
#* @tag model
#* @get /health
function() list(status = 'healthy')
#* Predict house price
#* @tag prediction
#* @param sqft:dbl Square footage
#* @param bedrooms:int Number of bedrooms
#* @get /predict
function(sqft = 1000, bedrooms = 3) {
pred <- predict(price_model, data.frame(sqft = as.numeric(sqft),
bedrooms = as.integer(bedrooms)))
list(predicted_price = round(as.numeric(pred), 2))
}Quick Check
In plumber, what is the difference between a query parameter (e.g. /greet?name=Alice) and a path parameter (e.g. /user/42)?
plumber and REST Recap
Key takeaways from Introduction to plumber and REST:
- REST: stateless, resource-oriented, uses standard HTTP verbs, returns JSON.
- plumber maps R functions to endpoints using
#*annotations above functions. pr('file.R')creates a router;pr_run(api, host, port)starts the server.#* @get /pathhandles GET;#* @post /pathhandles POST.- Path params:
/user/; query params:/search?term=foo. - Return named lists — plumber serialises them to JSON automatically.
- Swagger UI is auto-generated at
/__docs__/from your annotations.
# Complete minimal plumber API
library(plumber)
#* @apiTitle Simple Prediction API
#* Health check
#* @get /health
function() list(status = 'ok')
#* Predict mpg from weight
#* @param wt:dbl Car weight (1000 lbs)
#* @get /predict
function(wt = 3.0) {
pred <- predict(lm(mpg ~ wt, data = mtcars),
newdata = data.frame(wt = as.numeric(wt)))
list(wt = as.numeric(wt), predicted_mpg = round(pred, 2))
}
# Run:
# api <- pr('plumber.R')
# pr_run(api, port = 8000)Frequently asked questions
Is the “Introduction to Plumber and REST” lesson free?
Yes — the full text of “Introduction to Plumber and REST” is free to read here on the web, and the R Academy course includes 4 lessons in total. To practise it interactively (a built-in code editor and a 24/7 AI tutor) and unlock the rest of the R Academy course, upgrade to CoddyKit PRO.
What will I learn in “Introduction to Plumber and REST”?
Understand REST principles and annotate R functions as API endpoints. You practise R Academy with hands-on code you run directly in the browser, and a 24/7 AI tutor answers your questions as you work through the lesson.
Do I need any experience to start R Academy?
No prior experience is required. R Academy on CoddyKit is structured for beginners through advanced learners; this is — lesson 1 of 4, so you can start here or from the beginning and move at your own pace.
How long does the “Introduction to Plumber and REST” lesson take?
Most CoddyKit lessons take about 5–10 minutes. Each one is bite-sized and interactive, so you make steady progress and pick up exactly where you left off across the web and the app.
Can I write and run code in this R Academy lesson?
Yes. Every R Academy lesson includes a built-in code editor, so you write and run real code right in your browser and get instant AI feedback — no local setup required.
All lessons in this course
- Introduction to Plumber and REST
- Creating GET and POST Endpoints
- Authentication and API Security
- Deploying Plumber APIs to Production