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. This is lesson 1 of 4. Any 3 lessons of this learning path are free to read in full — after that, CoddyKit PRO unlocks every lesson, plus hands-on practice with a built-in code editor and a 24/7 AI tutor. It is part of the R Academy learning path, and your progress syncs across the web and the CoddyKit app. The R Academy course includes 4 lessons in total.
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)Learn R with an AI tutor — free
Write and run real code in your browser, get instant help from a 24/7 AI tutor, and pick up where you left off on the web or in the app.
- Courses
- 43
- Lessons
- 159
Frequently Asked Questions
Is the “Introduction to Plumber and REST” lesson free?
Yes — any 3 lessons of the R Academy learning path, including “Introduction to Plumber and REST”, are free to read in full here on the web. After that, CoddyKit PRO unlocks every lesson, plus interactive practice with a built-in code editor and a 24/7 AI tutor. The R Academy course includes 4 lessons in total.
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, so you can start here or from the beginning and move at your own pace. This is lesson 1 of 4.
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