plumber API as a package

Why package a plumber API?

There are many advantages to building R-based code as a package. You can use the DESCRIPTION file to store metadata related to the API, separate functions from endpoints in the R/ folder, include documentation and run tests.
In this example we'll build a simple plumber API as a package. The API will have two endpoints - /version will return the package version and /sum will sum two numbers.

Package folder structure and files

This is the package folder structure. It follows a typical R package structure but includes an entrypoint.R file in the root folder.

 1genericAPI
 2├── DESCRIPTION  
 3├── entrypoint.R  
 4├── LICENSE  
 5├── LICENSE.md
 6├── man  
 7├── NAMESPACE  
 8├── R  
 9│   ├── plumber.R  
10│   └── sum_numbers.R  
11├── tests  
12    ├── testthat.R  
13    └── testthat  
14        └── test-sum_numbers.R  

DESCRIPTION, NAMESPACE and license.md

DESCRIPTION is a typical DESCRIPTION file, as shown below.

 1Package: genericAPI
 2Title: A simple generic API
 3Version: 0.0.1
 4Authors@R: 
 5  person("Harvey", "Lieberman", "harvey.lieberman@novartis.com", role = c("aut", "cre"))
 6Description: An R package to demonstrate developing a plumber API as a package.
 7License: MIT + file LICENSE
 8Encoding: UTF-8
 9Imports:
10  pkgload,
11  plumber
12RoxygenNote: 7.2.1
13Suggests: 
14    testthat (>= 3.0.0)
15Config/testthat/edition: 3

NAMESPACE is built using roxygen2::roxygenise().

1# Generated by roxygen2: do not edit by hand
2
3import(pkgload)
4import(plumber)

entrypoint.R

When starting an API, the plumber package looks for plumber.R to parse as the plumber router definition. Alternatively, if an entrypoint.R file is found it will take precedence and be responsible for returning a runnable router.
The entrypoint.R is simple. It loads all function definitions and then points to the plumber.R file under the R/ folder.

1## load all functions
2pkgload::load_all()
3
4## start plumber
5pr <- plumber::plumb("./R/plumber.R")

R/plumber.R

There is no difference between this and the plumber.R file in a traditional plumber API. It holds the endpoints for the API. One advantage of building a package, however, is that the functions can be separated into other files. This makes the plumber.R file more succint, holding just the API endpoints.

In the code below we include a NULL function so that any packages required by the API are added to NAMESPACE. The /version endpoint returns the package version and the /sum endpoint calls a function called sum_numbers().

 1#* @apiTitle My generic plumber API
 2
 3#' plumber functions
 4#' 
 5#' plumber endpoints
 6#' 
 7#' @name plumber
 8#' @import pkgload
 9#' @import plumber
10NULL
11
12#* API version
13#* 
14#* return an API version
15#* 
16#* @serializer unboxedJSON
17#* 
18#* @get /version
19function() {
20  list(version = as.character(packageVersion("genericAPI")))
21}
22
23#* Sum
24#* 
25#* Sum two numbers
26#* 
27#* @param a a number
28#* @param b a number
29#* 
30#* @serializer unboxedJSON
31#* 
32#* @get /sum
33function(a, b) {
34  list(sum = sum_numbers(a, b))
35}

R/sum _ numbers.R

sum_numbers() is a simple function returning the sum of two numbers. Since it's included in a package we can take advantage of roxygen2 documentation. As it is only used within the API, this function does not have to be exported.

 1#' my simple function
 2#' 
 3#' A simple addition function
 4#' 
 5#' @param a numeric
 6#' @param b numeric
 7#' 
 8sum_numbers <- function(a, b) {
 9  tryCatch({
10    as.numeric(a) + as.numeric(b)
11  }, 
12  warning = function(e) {
13    "not numeric"
14  },
15  error = function(e) {
16    "error"
17  })
18}

testthat/test-sum _ numbers.R

Finally, we can add tests. The test-sum_numbers.R runs three simple tests to ensure our function is performing correctly.

1test_that("summation works", {
2  expect_equal(sum_numbers('1', '2'), 3)
3  expect_equal(sum_numbers(1, 2), 3)
4  expect_equal(sum_numbers(1, "A"), "not numeric")
5})

Deployment

Deployment to RStudio Connect is simple using the rsconnect pacakge

1rsconnect::deployAPI(
2  api = ".",
3  apiTitle = "sample_api",
4  appFiles = c("R", "entrypoint.R", "DESCRIPTION", "NAMESPACE"),
5  server = **RStudio Connect Server**,
6  forceUpdate = TRUE,
7  launch.browser = FALSE
8)

where **RStudio Connect Server** is the URL of the RStudio Connect server.

Testing the API

In the tests below **url** is the API URL and **apikey** is an RStudio Connect API key

test 1

1out <- httr::GET("**url**/version", httr::add_headers(Authorization = paste("Key", **apikey**)))
2httr::content(out)
3
4$version
5[1] "0.0.1"

test 2

1out <- httr::GET("**url**/sum?a=1&b=4", httr::add_headers(Authorization = paste("Key", **apikey**)))
2httr::content(out)
3
4$sum
5[1] 5

test 3

1out <- httr::GET("**url**/sum?a=1&b=none", httr::add_headers(Authorization = paste("Key", **apikey**)))
2httr::content(out)
3
4$sum
5[1] "not numeric"

Conclusion

Building a plumber API as an R package provides certain advantages such as documentation, a cleaner folder structure and testing.