R Academy · Lesson

Introduction to Plumber and REST

Understand REST principles and annotate R functions as API endpoints.

Lesson 1 of 413 steps

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 /path handles GET; #* @post /path handles 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)
Free to start

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

  1. Introduction to Plumber and REST
  2. Creating GET and POST Endpoints
  3. Authentication and API Security
  4. Deploying Plumber APIs to Production
← Back to R Academy