This document is a tutorial for the R package: plumber. A lot of examples and explanations come from the official documentation and this this article from R view.

I recommend using the “R-studio” IDE over the version 1.2 for this tutorial because it includes some plumber integration features.

Note: For this tutorial, I deployed an example API accessible from the UT-Biomet network at the address: http://192.168.101.44:8080.

You can then get the results of the codes presented in this document without installing anything on your computer. For example, you can access from your web browser to this address to get a summary of all the API functions presented in this tutorial: http://192.168.101.44:8080/__swagger__/

1 Introduction

The plumber R package (Trestle Technology, LLC 2017) allows users to expose existing R code as a service available to others on the Web through a “web API”.

Web APIs created by plumber use the Hypertext Transfer Protocol (HTTP) to communicate information. This kind of application is called RESTful API.

By this way, these APIs can be accessed by any programming languages, for example:

  • R: httr, RCurl packages
  • Python: requests library
  • JavaScript: Ajax request
  • Java: HttpUrlConnection class
  • Julia: HTTP package
  • And other…

1.1 About HTTP Requests and responses

The API resources are accessible through HTTP requests. They are composed of several parts:

  • URL or path: the address where the request is sent
  • Method: the method invoked on the endpoint. It can be: GET, POST, DELETE, or PUT.
  • Headers: some meta-data sent to the server
  • Body: data sent to the server outside of the headers, (mainly for POST and PUT requests)

The different methods are for different usages:

  • GET: is used to get a resource or collection of resources
  • POST: is used to create a resource or collection of resources
  • DELETE: is used to delete the existing resource or the collection of resources
  • PUT: is used to update the existing resource or collection of resources

2 Installation

The latest stable version of plumber is hosted on CRAN and can be installed by the following command in R:

install.packages("plumber")

The latest development version can also be installed from GitHub:

library(devtools)
install_github("trestletech/plumber")

3 First look on a plumber API

3.1 Open a new plumber.R file

After installing the package restart R-studio and create a new plumber.R file :

File >> New File >> Plumber API…

Enter the API name and the path of the file in the following window:

An R file named plumber.R (the conventional name for plumber APIs) containing an example API will open in RStudio.

https://support.rstudio.com/hc/en-us/articles/200526207-Using-Projects

New plumber APIs can also be created within an R project:

File >> New Project… >> New Directory >> Plumber API Project

I highly recommend the use of “R projects” for any R development ! For more information: https://support.rstudio.com/hc/en-us/articles/200526207-Using-Projects

3.2 Plumber API code structure

Let’s take a look at this example file:

The first part is just the file header and the package loading:

#
# This is a Plumber API. You can run the API by clicking
# the 'Run API' button above.
#
# Find out more about building APIs with Plumber here:
#
#    https://www.rplumber.io/
#

library(plumber)

Just after came the API description:

#* @apiTitle Plumber Example API
  • Here only the API name is set up using the syntax #* @apiTitle

Finally came three Plumber “endpoints.” echo, plot and sum:

#* Echo back the input
#* @param msg The message to echo
#* @get /echo
function(msg = "") {
    list(msg = paste0("The message is: '", msg, "'"))
}

#* Plot a histogram
#* @png
#* @get /plot
function() {
    rand <- rnorm(100)
    hist(rand)
}

#* Return the sum of two numbers
#* @param a The first number to add
#* @param b The second number to add
#* @post /sum
function(a, b) {
    as.numeric(a) + as.numeric(b)
}
  • These endpoints functions and preceded by a set of comments starting by #*. Theses annotation set up the API’s endpoints.

3.3 Run the example API

The API can be launched by clicking on the “Run API” button from R-Studio. I recommend using the “Run External” option to run the API in your default web browser.

Once the API is running you should see this page in your web browser:

This message should also appear in the R console: Starting server to listen on port ????, with ???? the port number where the API is connected (4727 in my case).

Congratulation the example API is now accessible on your local machine!

You can now make some request to this API. In your web browser go to the address (replacing XXXX by your port number):

The /echo endpoint should show output resembling:

{"msg": ["The message is: ''"]}

The /plot endpoint should show output resembling:

These endpoints can be accessed on the example API hosted in the lab at the following addresses:

4 Create our custom API

Let’s now create our own API!

4.1 plumber syntax

Developing plumber API is simply a matter of providing specialized R comments before R functions. plumber recognizes both #* (or #' like the package roxygen2) comments. Some specific arguments specifying the API’s setup can be written using @.

For example, the title of the API is specified by this line using the @apiTitle argument.

#* @apiTitle Plumber Example API

4.2 API description

First let’s describe our API. The following is a list of some optional arguments that can be used for describing your API.

##### API description #####

#* @apiTitle Plumber Example API
#* @apiDescription API example for the Plumber tutorial
#* @apiVersion 1.0.42
#* @apiTag default_example Endpoints of the default plumber.R example
#* @apiTag group1 Endpoints of the group 1
#* @apiTag group2 Endpoints of the group 2
#* @apiContact @email juliendiot@ut-biomet.org
  • @apiTitle, @apiDescription, @apiVersion, and @apiContact are quite explicit.
  • @apiTag: Allow to specify some groups for the API functions. The name of the group and its description follow this parameter. A function can be assigned to a group with @tag followed by the group name. The endpoints will appear grouped in the “Swagger” page of the API.

4.3 Endpoints, API functions returning a result.

Endpoints are the final step in the process of serving a request. An endpoint can simply be viewed as a function generating a response to a particular request.

Let’s create a very simple endpoint with no arguments returning the time and time zone of the API’s server.

#* Return the time of the API's server
#* @get /time
function(){
  list(time = Sys.time(),
       timezone = Sys.timezone())
}

The first line is the function description.

The second line specifies the method and the path to access this function. Here, the above function run when a request with the GET method is sent at the path /time

4.4 Function’s arguments

Some arguments can be specified using the syntax #* @param followed by the argument name, and its description

#* Return square root of x
#* @param x a number
#* @get /sqrt
function(x){
  sqrt(as.numeric(x))
}

These parameters at specified with the syntax: ?param1=Value1&param2=value2... at the end of the request’s URL.

Notes: Arguments are passed to functions as character strings. The as.numeric function must be called to manage numbers.

It is also possible to set “Dynamic Routes”. In these cases, the arguments are specified in the path:

#* Return a subset of Iris data according to the specie
#* @get /iris/<spec>
function(spec) {
  subset(iris, Species == spec)
}

4.4.1 The req and res variables

It is possible to access and modify the request and the response object in any functions of plumber directly. The function definition must specify req or res variables. For example, we can improve the /sqrt endpoint to return an error status if the request is not good (if x is not a positive number):

#* Return square root of x
#* @param x a number
#* @get /sqrt
function(x, res){
  if (missing(x)) {
    res$status <- 400 # Bad Request
    return(list(error = "x must be specified."))
  }

  x <- as.numeric(x)
  if (is.na(x) | x < 0) {
    res$status <- 400 # Bad Request
    return(list(error = "x must be positive number."))
  }

  sqrt(x)
}

4.5 Outputs

plumber must transform the objects returned by the R functions in another format the client can understand. It is called “serialization”. The default serializer is to the JSON (JavaScript Object Notation) format, but others can be used:

  • @json (default)
  • @html
  • @jpeg the size and width can be specified using (width = WW, height = HH)
  • @png
  • @serializer htmlwidget: for objects from packages plotly, DT
  • @serializer unboxedJSON: to return atomic value.

This parameter should be placed in the function’s comments.

#* JSON output
#* @tag outputs
#* @get /iris
#* @json
function(spec) {
  iris
}

#* HTML output
#* @tag outputs
#* @get /html
#* @html
function(){
  "<html><h1>Some HTML code:</h1><p>Hello world!</p></html>"
}

#* jpeg output
#* @tag outputs
#* @get /jpeg
#* @jpeg (width = 500, height = 500)
function(){
  curve(dnorm,-5,5)
}

#* png output
#* @tag outputs
#* @get /png
#* @png (width = 300, height = 300)
function(){
  curve(dnorm,-5,5)
}

#* plotly (htmlwidget) output
#* @tag outputs
#* @get /plotly
#* @serializer htmlwidget
function(){
  x <- seq(-5, 5, length.out = 500)
  d <- dnorm(x)

  plot_ly(
    type = 'scatter',
    mode = 'lines',
    name = paste("N(0;1)"),
    x = x,
    y = d,
    text = paste("f(X) =", round(d, 3),
                 "\nX =", round(x, 3)),
    hoverinfo = 'text'
  )
}

#* datatable (htmlwidget) output
#* @tag outputs
#* @get /datatable
#* @serializer htmlwidget
function(){
  datatable(iris)
}

The serializer above are the ones built into the package. However, other formats can be interesting like “pdf”, “XML”…

In these cases, you can leverage the @serializer contentType annotation. plumber doesn’t serialize the response. It specifies the contentType header according to what is defined by list(type="content/type") (A list of some content types is available in the appendix). For example:

#* pdf output
#* @tag outputs
#* @get /pdf
#* @serializer contentType list(type="application/pdf")
function(){
  tmp <- tempfile()
  pdf(tmp)
  plot(1:10, type = "b")
  text(4, 8, "PDF from plumber!")
  text(6, 2, paste("The time is", Sys.time()))
  dev.off()

  readBin(tmp, "raw", n = file.info(tmp)$size)
}

It is also possible to write the content body directly:

#* Endpoint that bypasses serialization
#* @tag outputs
#* @get /no_serialization
function(res){
  res$body <- "the content of the body"
  res
}

4.6 Work with differents HTTP methods

As mention before, there are four HTTP methods usable in Rest API:

  • GET: is used to get a resource or collection of resources
  • POST: is used to create a resource or collection of resources
  • PUT: is used to update the existing resource or collection of resources
  • DELETE: is used to delete the existing resource or the collection of resources

Let’s try to implement these methods by constructing a “messages board” (naive and straightforward) API where we can send and read messages. This API should be able to:

  • Add new messages to the board
  • Return one or several messages to the client
  • Modify messages
  • Delete messages

The messages are stored in a simple .txt file on the server as a table. A message is composed of 5 parts:

  • The name of the sender
  • The message subject
  • The message content
  • An id
  • The date and time of the message

4.6.1 Add a new message

We want to create a new resource so we should use the POST method.

The client should specify 3 parameters for this request:

  • from: who is the sender
  • subject: the message subject
  • content: the message content

The API will automatically create the ID and the time of the message.

#* Add a message to the "Public Messages Board"
#* @tag message_Board
#* @param from The sender
#* @param subject The message subject
#* @param content The message content
#* @post /messages
function(from, content, subject="no subject"){

  newMessage = data.frame(id = NA,
                          from = from,
                          subject = subject,
                          content = content,
                          time = as.character(Sys.time()))

  file <- "./data/messages.txt"

  if (file.exists(file)) {
    messages <- read.table(file,
                           header = T,
                           sep = '\t')

    newMessage$id <- max(messages$id) + 1
    write.table(newMessage, file, append = TRUE,
                sep = "\t",
                row.names = F,
                col.names = F)
    out <- "messages added !"
  } else {
    newMessage$id <- 1
    write.table(newMessage,
                file,
                sep = "\t",
                row.names = F,
                col.names = T)
    out <- "new file created, messages added !"
  }
  return(list(out = out,
              id = newMessage$id))
}

4.6.2 Get some messages

We want to access to one or several messages so we should use the GET method:

Two endpoints are created one to get all the messages: /messages and another to filter the messages according to their id, sender or subject.

#* Return all messages of the message board
#* @tag message_Board
#* @get /messages
function() {

  file <- "./data/messages.txt"
  if (file.exists(file)) {
    messages <- read.table(file,
                           header = T,
                           sep = '\t')
    return(messages)
  }
}

#* Return a subset of messages according to param
#* @tag message_Board
#* @get /messages/<param>
function(param) {

  file <- "./data/messages.txt"
  if (file.exists(file)) {
    messages <- read.table(file,
                           header = T,
                           sep = '\t')

    if (param %in% unique(messages$from)) {

      return(subset(messages, from == param))

    } else if (param %in% unique(messages$subject)) {

      return(subset(messages, subject == param))

    } else if (as.numeric(param) %in% messages$id) {

      return(messages[messages$id ==  as.numeric(param),])

    }
  }
}

4.6.3 Edit an existing message

We want to access to modify a message so we should use the PUT method:

The request must provide the message’s id to modify and the new values for the sender, the content, or the subject.

#* Edit a message
#* @tag message_Board
#* @param id id of the message
#* @param from The sender
#* @param subject The message subject
#* @param content The message content
#* @put /messages
function(id, from=NULL, content=NULL, subject=NULL){
  id <- as.numeric(id)
  file <- "./data/messages.txt"

  if (file.exists(file)) {

    messages <- read.table(file,
                           header = T,
                           sep = '\t')

    if (id %in% messages$id) {

      if (!is.null(from)) {
        messages[messages$id == id,]$from <- from
      }
      if (!is.null(content)) {
        messages[messages$id == id,]$content <- content
      }
      if (!is.null(subject)) {
        messages[messages$id == id,]$subject <- subject
      }
      messages[messages$id == id,]$time <- as.character(Sys.time())

      write.table(messages,
                  file,
                  sep = "\t",
                  row.names = F,
                  col.names = T)
      return("Messages edited ! ")

    }
  }
}

4.6.4 Delete a message

We want to delete a message so we should use the DELETE method. The deleted message must be identified with its id.

#* Delete a message
#* @tag message_Board
#* @delete /messages/<id>
function(id){
  id <- as.numeric(id)
  file <- "./data/messages.txt"

  if (file.exists(file)) {

    messages <- read.table(file,
                           header = T,
                           sep = '\t')

    if (id %in% messages$id) {
      messages <- messages[messages$id != id,]

      write.table(messages,
                  file,
                  sep = "\t",
                  row.names = F,
                  col.names = T)
      return("Messages deleted ! ")

    }
  }
}

See on the online API:

The GET request can be made in a web browser:

The other requests can be made on the “Swagger” pages of the application:

4.7 Filters

Filters are plumber functions used to define a “pipeline” for handling incoming requests. Unlike endpoints, a request may go through multiple Plumber filters before a response is generated.

The request is passed through the filters in order of appearance in the code. Filters can do one of three things in handling a request:

  • Forward control onto the next handler, potentially after mutating the request.
  • Return a response itself and not forward to subsequent handlers
  • Throw an error

One common use case is to use a filter as a request logger:

#* Log some information about the incoming request
#* @filter logger
function(req){
  cat(as.character(Sys.time()), "-",
      req$REQUEST_METHOD, req$PATH_INFO, "-",
      req$HTTP_USER_AGENT, "@", req$REMOTE_ADDR, "\n")
  plumber::forward()
}

4.8 Debbuging

In R, print() or cat() can be used to print out some state. For instance, cat("i is currently: ", i) could be inserted in your code to help you ensure that the variable i is what it should be at that point in your code.

This approach is equally viable with plumber. When developing your Plumber API in an interactive environment, this debugging output will be logged to the same terminal where you called run() on your API. In a non-interactive production environment, these messages will be included in the API server logs for later inspection.

Print debugging is an obvious starting point, but most developers eventually wish for something more powerful. In R, this capacity is built into the browser() function. If you’re unfamiliar, browser() pauses the execution of some function and gives you an interactive session in which you can inspect the current value of internal variables or even proceed through your function one statement at a time. (By the way, this function is also very beneficial for debugging Shiny applications or function called with an apply function).

You can leverage browser() when developing your APIs locally by adding a browser() call in one of your endpoints.

This is also an excellent way to get your hands dirty with Plumber and get better acquainted with how things behave at a low level. Consider the following API endpoint:

#* @get /
function(req, res){
  browser()
  list(a = 123)
}

If you run this API locally and then visit the API in a web browser, you’ll see your R session switch into debug mode when the request arrives, allowing you to look inside the req and res objects.

Note: Of course, the browser function must not be present in any “in production” API, so the last endpoint is not in the deployed API of the lab.

5 Access APIs from R

The package httr provide some tools to send HTTP requests to an online API. The functions GET, POST, PUT, DELETE call the corresponding methods:

library(httr)
resGET <- GET("http://192.168.101.44:8080/time")
print(resGET)
##  Response [http://192.168.101.44:8080/time]
##    Date: 2019-08-21 08:14
##    Status: 200
##    Content-Type: application/json
##    Size: 58 B
print(content(resGET))
## $time
## $time[[1]]
## [1] "2019-08-21 17:24:06"
## 
## 
## $timezone
## $timezone[[1]]
## [1] "Asia/Tokyo"

Let’s add a message on the “messages board” from R:

# messages specification
from <- "R"
subject <- "POST request"
content <- "I did a POST request from R !"

# write the url for the request
url <- paste0("http://192.168.101.44:8080/messages?from=", from,
              "&subject=", subject,
              "&content=", content )
url <- gsub(" ", "%20", url) # remove spaces

# send the request
resPOST <- POST(url)

# response visualisation
print(resPOST)
print(content(resPOST))
id <- unlist(content(resPOST)$id)
##  Response [http://192.168.101.44:8080/messages?from=R&subject=POST%20request&content=I%20did%20a%20POST%20request%20from%20R%20!]
##    Date: 2019-08-21 08:01
##    Status: 200
##    Content-Type: application/json
##    Size: 38 B
##  
##  $out
##  $out[[1]]
##  [1] "messages added !"
##  
##  
##  $id
##  $id[[1]]
##  [1] 2

The package jsonlite provides functions to manage the JSON format in R:

library(jsonlite)
url <- paste0("http://192.168.101.44:8080/messages/", id)
resGET <- GET(url)

print(resGET)
print(fromJSON(content(resGET, "text"), flatten = TRUE))
##  Response [http://192.168.101.44:8080/messages/2]
##    Date: 2019-08-21 08:01
##    Status: 200
##    Content-Type: application/json
##    Size: 118 B
##  
##    id from      subject                       content                time
##  1 2    R POST request I did a POST request from R ! 2019-08-21 17:01:45
DELETE(url)
##  Response [http://192.168.101.44:8080/messages/2]
##    Date: 2019-08-21 08:01
##    Status: 200
##    Content-Type: application/json
##    Size: 23 B

Note: The codes above do not truly generate the previous outputs when the .rmd file of this tutorial is rendered.

Indeed, the online API can render this report (see next section The main advantage of plumber). However, a plumber API can’t call them selfs because they use a single thread only. In order to avoid any bug, the results above had been implemented manually using the cat function in the .rmd file. These results are still the same than what you get when running the code in an interactive R session.

6 The main advantage of plumber

The main advantage of plumber is it allow to integrate all the power of the R language in an API. It is particularly interesting to provide features which would be challenging to implement in other languages.

For example, plumber API can quickly use statistical models developed in R. The following endpoint returns the result of a KNN prediction model determining the species of an iris flower from the petal and sepal width and length. The code generating the model is given in the appendix.

#* Predict species of Iris
#* @tag Rpower
#* @param SepalLength
#* @param SepalWidth
#* @param PetalLength
#* @param PetalWidth
#* @get /predictIris
function(SepalLength, SepalWidth, PetalLength, PetalWidth){

  load("irisKNNmodel.Rdata")

  newdata <- data.frame(
    Sepal.Length = as.numeric(SepalLength),
    Sepal.Width = as.numeric(SepalWidth),
    Petal.Length = as.numeric(PetalLength),
    Petal.Width = as.numeric(PetalWidth)
  )
  as.character(predict(irisKNNmodel, newdata = newdata))
}

R provides useful packages that can also be integrated into an API. This endpoint generates an HTML document (this tutorial) from a Rmarkdown file:

#* Run Rmarkdown to generate a plumber tutorial
#* @tag Rpower
#* @get /plumbertuto
#* @html
function(){
  file <- tempfile("PlumberTuto", fileext = ".html")
  render("./PlumberTuto.Rmd",
         output_file = file,
         envir = new.env(parent = globalenv()),
         encoding = "UTF-8")
  HTML <- paste(readLines(file), collapse = "\n")
  HTML
}

7 Advanced features

The plumber package provides some more advanced features. These features will not be detailed in this tutorial, but resources can be found on the official documentation:

9 Appendix

9.1 Content-type

  • html : 'text/html; charset : UTF-8'
  • htm : 'text/html; charset : UTF-8'
  • js : 'text/javascript'
  • css : 'text/css'
  • png : 'image/png'
  • jpg : 'image/jpeg'
  • jpeg : 'image/jpeg'
  • gif : 'image/gif'
  • svg : 'image/svg+xml'
  • txt : 'text/plain'
  • pdf : 'application/pdf'
  • ps : 'application/postscript'
  • xml : 'application/xml'
  • m3u : 'audio/x-mpegurl'
  • m4a : 'audio/mp4a-latm'
  • m4b : 'audio/mp4a-latm'
  • m4p : 'audio/mp4a-latm'
  • mp3 : 'audio/mpeg'
  • wav : 'audio/x-wav'
  • m4u : 'video/vnd.mpegurl'
  • m4v : 'video/x-m4v'
  • mp4 : 'video/mp4'
  • mpeg : 'video/mpeg'
  • mpg : 'video/mpeg'
  • avi : 'video/x-msvideo'
  • mov : 'video/quicktime'
  • ogg : 'application/ogg'
  • swf : 'application/x-shockwave-flash'
  • doc : 'application/msword'
  • xls : 'application/vnd.ms-excel'
  • ppt : 'application/vnd.ms-powerpoint'
  • xlsx : 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
  • xltx : 'application/vnd.openxmlformats-officedocument.spreadsheetml.template'
  • potx : 'application/vnd.openxmlformats-officedocument.presentationml.template'
  • ppsx : 'application/vnd.openxmlformats-officedocument.presentationml.slideshow'
  • pptx : 'application/vnd.openxmlformats-officedocument.presentationml.presentation'
  • sldx : 'application/vnd.openxmlformats-officedocument.presentationml.slide'
  • docx : 'application/vnd.openxmlformats-officedocument.wordprocessingml.document'
  • dotx : 'application/vnd.openxmlformats-officedocument.wordprocessingml.template'
  • xlam : 'application/vnd.ms-excel.addin.macroEnabled.12'
  • xlsb : 'application/vnd.ms-excel.sheet.binary.macroEnabled.12'

9.2 KNN on Iris data

R code generating the KNN prediction model:

# Author: Julien Diot juliendiot@ut-biomet.org
# 2019 The University of Tokyo
#
# Description:
# A simple KNN model for the iris dataset.

#### PACKAGES ####
library(caret)


#### OPTIONS ####
# options(stringsAsFactors = FALSE)


#### CODE ####

# Import data
data(iris)


# train model
trControl <- trainControl(method = "boot",
                          number = 100)
method <- "knn"

(irisKNNmodel <- train(
  Species ~ .,
  data = iris,
  method = method,
  tuneGrid = data.frame(k = seq(1, 50, by = 2)),
  # tuneLength = 10,
  trControl = trControl))
ggplot(irisKNNmodel, metric = "Accuracy")

save(irisKNNmodel, file = "irisKNNmodel.Rdata")
print(sessionInfo(), locale = FALSE)
## R version 4.0.2 (2020-06-22)
## Platform: x86_64-pc-linux-gnu (64-bit)
## Running under: Ubuntu 20.04.1 LTS
## 
## Matrix products: default
## BLAS:   /usr/local/lib/R/lib/libRblas.so
## LAPACK: /usr/local/lib/R/lib/libRlapack.so
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## loaded via a namespace (and not attached):
##  [1] compiler_4.0.2  magrittr_1.5    tools_4.0.2     htmltools_0.5.0
##  [5] yaml_2.2.1      stringi_1.4.6   rmarkdown_2.3   knitr_1.29     
##  [9] stringr_1.4.0   xfun_0.15       digest_0.6.25   rlang_0.4.6    
## [13] evaluate_0.14