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.