Logo

The Data Daily

API as a package: Structure | R-bloggers

API as a package: Structure | R-bloggers

% create_routes() %>% generate_api() and nothing about this code is specific to my current package so is transferable. As a concrete, but very much simplified example, I might have the following collection of files/annotations under /inst/extdata/api/routes ## File: /example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } ## File: /test.R #* @get /is_alive function() { list(alive = TRUE) } ## File: /nested/example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } which would give me get_internal_routes() %>% create_routes() %>% generate_api() # # Plumber router with 0 endpoints, 4 filters, and 3 sub-routers. # # Use `pr_run()` on this object to start the API. # ├──[queryString] # ├──[body] # ├──[cookieParser] # ├──[sharedSecret] # ├──/example # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/echo (GET) # ├──/nested # │ ├──/example # │ │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ │ ├──[queryString] # │ │ ├──[body] # │ │ ├──[cookieParser] # │ │ ├──[sharedSecret] # │ │ └──/echo (GET) # ├──/test # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/is_alive (GET) This {cookieCutter} example is available to view at our Github blog repo. Basic testing In my real project I refrained from having any actual function definitions being made in inst/. Instead each function that was part of the exposed API was a proper exported function from my package (additionally filenames for said functions followed a regular structure too of api_.R). This allows for leveraging {testthat} against the logic of each of the functions as well as using other tools like {lintr} and ensuring that dependencies, documentation etc are all dealt with appropriately. Testing individual functions that will be exposed as routes can be a little different to other R functions in that the objects passed as arguments come from a request. As alluded to in the introduction I will prepare another blog post detailing some elements of testing for API as a package but a short snippet that I found particularly helpful for testing that a running API is functioning as I expect is included here. The following code could be used to set up (and subsequently tear down) a running API that is expecting requests for a package cookieCutter # tests/testthat/setup.R ## run before any tests # pick a random available port to serve your app locally port = httpuv::randomPort() # start a background R process that launches an instance of the API # serving on that random port running_api = callr::r_bg( function(port) { dir = cookieCutter::get_internal_routes() routes = cookieCutter::create_routes(dir) api = cookieCutter::generate_api(routes) api$run(port = port, host = "0.0.0.0") }, list(port = port) ) # Small wait for the background process to ensure it # starts properly Sys.sleep(1) ## run after all tests withr::defer(running_api$kill(), testthat::teardown_env()) A simple test to ensure that our is_alive endpoint works then might look like test_that("is alive", { res = httr::GET(glue::glue("http://0.0.0.0:{port}/test/is_alive")) expect_equal(res$status_code, 200) }) Logging {shiny} has some useful packages for adding logging, in particular {shinylogger} is very helpful at giving you plenty of logging for little effort on my part as the user. As far as I could find nothing similar exists for {plumber} so I set up a bunch of hooks, using the {logger} package to write information to both file and terminal. Since that could form it’s own blogpost I will save that discussion for the future. For updates and revisions to this article, see the original post" />
% create_routes() %>% generate_api() and nothing about this code is specific to my current package so is transferable. As a concrete, but very much simplified example, I might have the following collection of files/annotations under /inst/extdata/api/routes ## File: /example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } ## File: /test.R #* @get /is_alive function() { list(alive = TRUE) } ## File: /nested/example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } which would give me get_internal_routes() %>% create_routes() %>% generate_api() # # Plumber router with 0 endpoints, 4 filters, and 3 sub-routers. # # Use `pr_run()` on this object to start the API. # ├──[queryString] # ├──[body] # ├──[cookieParser] # ├──[sharedSecret] # ├──/example # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/echo (GET) # ├──/nested # │ ├──/example # │ │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ │ ├──[queryString] # │ │ ├──[body] # │ │ ├──[cookieParser] # │ │ ├──[sharedSecret] # │ │ └──/echo (GET) # ├──/test # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/is_alive (GET) This {cookieCutter} example is available to view at our Github blog repo. Basic testing In my real project I refrained from having any actual function definitions being made in inst/. Instead each function that was part of the exposed API was a proper exported function from my package (additionally filenames for said functions followed a regular structure too of api_.R). This allows for leveraging {testthat} against the logic of each of the functions as well as using other tools like {lintr} and ensuring that dependencies, documentation etc are all dealt with appropriately. Testing individual functions that will be exposed as routes can be a little different to other R functions in that the objects passed as arguments come from a request. As alluded to in the introduction I will prepare another blog post detailing some elements of testing for API as a package but a short snippet that I found particularly helpful for testing that a running API is functioning as I expect is included here. The following code could be used to set up (and subsequently tear down) a running API that is expecting requests for a package cookieCutter # tests/testthat/setup.R ## run before any tests # pick a random available port to serve your app locally port = httpuv::randomPort() # start a background R process that launches an instance of the API # serving on that random port running_api = callr::r_bg( function(port) { dir = cookieCutter::get_internal_routes() routes = cookieCutter::create_routes(dir) api = cookieCutter::generate_api(routes) api$run(port = port, host = "0.0.0.0") }, list(port = port) ) # Small wait for the background process to ensure it # starts properly Sys.sleep(1) ## run after all tests withr::defer(running_api$kill(), testthat::teardown_env()) A simple test to ensure that our is_alive endpoint works then might look like test_that("is alive", { res = httr::GET(glue::glue("http://0.0.0.0:{port}/test/is_alive")) expect_equal(res$status_code, 200) }) Logging {shiny} has some useful packages for adding logging, in particular {shinylogger} is very helpful at giving you plenty of logging for little effort on my part as the user. As far as I could find nothing similar exists for {plumber} so I set up a bunch of hooks, using the {logger} package to write information to both file and terminal. Since that could form it’s own blogpost I will save that discussion for the future. For updates and revisions to this article, see the original post" />
% create_routes() %>% generate_api() and nothing about this code is specific to my current package so is transferable. As a concrete, but very much simplified example, I might have the following collection of files/annotations under /inst/extdata/api/routes ## File: /example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } ## File: /test.R #* @get /is_alive function() { list(alive = TRUE) } ## File: /nested/example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } which would give me get_internal_routes() %>% create_routes() %>% generate_api() # # Plumber router with 0 endpoints, 4 filters, and 3 sub-routers. # # Use `pr_run()` on this object to start the API. # ├──[queryString] # ├──[body] # ├──[cookieParser] # ├──[sharedSecret] # ├──/example # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/echo (GET) # ├──/nested # │ ├──/example # │ │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ │ ├──[queryString] # │ │ ├──[body] # │ │ ├──[cookieParser] # │ │ ├──[sharedSecret] # │ │ └──/echo (GET) # ├──/test # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/is_alive (GET) This {cookieCutter} example is available to view at our Github blog repo. Basic testing In my real project I refrained from having any actual function definitions being made in inst/. Instead each function that was part of the exposed API was a proper exported function from my package (additionally filenames for said functions followed a regular structure too of api_.R). This allows for leveraging {testthat} against the logic of each of the functions as well as using other tools like {lintr} and ensuring that dependencies, documentation etc are all dealt with appropriately. Testing individual functions that will be exposed as routes can be a little different to other R functions in that the objects passed as arguments come from a request. As alluded to in the introduction I will prepare another blog post detailing some elements of testing for API as a package but a short snippet that I found particularly helpful for testing that a running API is functioning as I expect is included here. The following code could be used to set up (and subsequently tear down) a running API that is expecting requests for a package cookieCutter # tests/testthat/setup.R ## run before any tests # pick a random available port to serve your app locally port = httpuv::randomPort() # start a background R process that launches an instance of the API # serving on that random port running_api = callr::r_bg( function(port) { dir = cookieCutter::get_internal_routes() routes = cookieCutter::create_routes(dir) api = cookieCutter::generate_api(routes) api$run(port = port, host = "0.0.0.0") }, list(port = port) ) # Small wait for the background process to ensure it # starts properly Sys.sleep(1) ## run after all tests withr::defer(running_api$kill(), testthat::teardown_env()) A simple test to ensure that our is_alive endpoint works then might look like test_that("is alive", { res = httr::GET(glue::glue("http://0.0.0.0:{port}/test/is_alive")) expect_equal(res$status_code, 200) }) Logging {shiny} has some useful packages for adding logging, in particular {shinylogger} is very helpful at giving you plenty of logging for little effort on my part as the user. As far as I could find nothing similar exists for {plumber} so I set up a bunch of hooks, using the {logger} package to write information to both file and terminal. Since that could form it’s own blogpost I will save that discussion for the future. For updates and revisions to this article, see the original post" />
This is part one of our three part series Part 1: API as a package: Structure (this post) Part 2: API as a package: Logging (to be published) Part 3: API as a package: Testing (to be published) Introduction At Jumping Rivers we were recently tasked with taking a prototype application built in {...
" />
This is part one of our three part series Part 1: API as a package: Structure (this post) Part 2: API as a package: Logging (to be published) Part 3: API as a package: Testing (to be published) Introduction At Jumping Rivers we were recently tasked with taking a prototype application built in {...
">
% create_routes() %>% generate_api() and nothing about this code is specific to my current package so is transferable. As a concrete, but very much simplified example, I might have the following collection of files/annotations under /inst/extdata/api/routes ## File: /example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } ## File: /test.R #* @get /is_alive function() { list(alive = TRUE) } ## File: /nested/example.R # Taken from plumber quickstart documentation # https://www.rplumber.io/articles/quickstart.html #* @get /echo function(msg="") { list(msg = paste0("The message is: '", msg, "'")) } which would give me get_internal_routes() %>% create_routes() %>% generate_api() # # Plumber router with 0 endpoints, 4 filters, and 3 sub-routers. # # Use `pr_run()` on this object to start the API. # ├──[queryString] # ├──[body] # ├──[cookieParser] # ├──[sharedSecret] # ├──/example # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/echo (GET) # ├──/nested # │ ├──/example # │ │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ │ ├──[queryString] # │ │ ├──[body] # │ │ ├──[cookieParser] # │ │ ├──[sharedSecret] # │ │ └──/echo (GET) # ├──/test # │ │ # Plumber router with 1 endpoint, 4 filters, and 0 sub-routers. # │ ├──[queryString] # │ ├──[body] # │ ├──[cookieParser] # │ ├──[sharedSecret] # │ └──/is_alive (GET) This {cookieCutter} example is available to view at our Github blog repo. Basic testing In my real project I refrained from having any actual function definitions being made in inst/. Instead each function that was part of the exposed API was a proper exported function from my package (additionally filenames for said functions followed a regular structure too of api_.R). This allows for leveraging {testthat} against the logic of each of the functions as well as using other tools like {lintr} and ensuring that dependencies, documentation etc are all dealt with appropriately. Testing individual functions that will be exposed as routes can be a little different to other R functions in that the objects passed as arguments come from a request. As alluded to in the introduction I will prepare another blog post detailing some elements of testing for API as a package but a short snippet that I found particularly helpful for testing that a running API is functioning as I expect is included here. The following code could be used to set up (and subsequently tear down) a running API that is expecting requests for a package cookieCutter # tests/testthat/setup.R ## run before any tests # pick a random available port to serve your app locally port = httpuv::randomPort() # start a background R process that launches an instance of the API # serving on that random port running_api = callr::r_bg( function(port) { dir = cookieCutter::get_internal_routes() routes = cookieCutter::create_routes(dir) api = cookieCutter::generate_api(routes) api$run(port = port, host = "0.0.0.0") }, list(port = port) ) # Small wait for the background process to ensure it # starts properly Sys.sleep(1) ## run after all tests withr::defer(running_api$kill(), testthat::teardown_env()) A simple test to ensure that our is_alive endpoint works then might look like test_that("is alive", { res = httr::GET(glue::glue("http://0.0.0.0:{port}/test/is_alive")) expect_equal(res$status_code, 200) }) Logging {shiny} has some useful packages for adding logging, in particular {shinylogger} is very helpful at giving you plenty of logging for little effort on my part as the user. As far as I could find nothing similar exists for {plumber} so I set up a bunch of hooks, using the {logger} package to write information to both file and terminal. Since that could form it’s own blogpost I will save that discussion for the future. For updates and revisions to this article, see the original post " />

Images Powered by Shutterstock