--- title: "Building an API Package" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Building an API Package} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, include = FALSE} knitr::opts_chunk$set( eval = FALSE, collapse = TRUE, comment = "#>" ) ``` ```{r setup} library(beekeeper) ``` The goal of beekeeper is to streamline and standardize the creation of high-quality API-wrapper packages. The process works best for APIs that follow the [OpenAPI Specification](https://spec.openapis.org/oas/v3.0.0) (the "OAS"). Some APIs that follow the OAS can be found on [APIs.guru](https://apis.guru/). We will use the [APIs.guru API](https://api.apis.guru/v2/openapi.yaml) (no authentication) and the [OpenFEC API](https://api.apis.guru/v2/specs/fec.gov/1.0/openapi.yaml) (authentication using an API key) as examples. ## Step 0: Create your package To start, first create a package. We recommend that you start with `usethis::create_package()` to streamline the general package-creation steps. You can then get basic API calls working in that package through a two-step process. ## Step 1: Configure your package If your target API follows the OAS, you can use your API's OpenAPI Document to configure your package. ```{r config-guru} # Note: Running these commands will create or overwrite "_beekeeper.yml" in your # working directory. url("https://api.apis.guru/v2/openapi.yaml") |> use_beekeeper( api_abbr = "guru" ) # Or for the FEC API url("https://api.apis.guru/v2/specs/fec.gov/1.0/openapi.yaml") |> use_beekeeper( api_abbr = "fec" ) ``` ## Step 2: Generate your package skeleton With a valid `_beekeeper.yml` file, you can generate the rest of the package. Right now the package will only export a function to call the API, but eventually this process will also generate functions for the endpoints specified in the API's OpenAPI document. ```{r generate} # Note: Running this command will create or overwrite files in your R and # tests/testthat directories. generate_pkg() ``` `generate_pkg()` creates files defining the package in the `R` directory, and tests in the `tests/testthat` directory. ## The generated package ### 010-call.R The first file generated for the package is `R/010-call.R`. This file defines a function that can be used to call the API. ```{r fec_call_api} # Set up the basic call once at package build. fec_req_base <- nectar::req_setup( "https://api.open.fec.gov/v1", user_agent = "fecapi (https://github.com/jonthegeek/fecapi)" ) #' Call the OpenFEC API #' #' Generate a request to an OpenFEC endpoint. #' #' @inheritParams nectar::req_modify #' @param api_key An API key provided by the API provider. This key is not #' clearly documented in the API description. Check the API documentation for #' details. #' #' @return The response from the endpoint. #' @export fec_call_api <- function(path, query = NULL, body = NULL, method = NULL, api_key = Sys.getenv("FEC_API_KEY")) { req <- nectar::req_modify( fec_req_base, path = path, query = query, body = body, method = method ) req <- .fec_req_auth(req, api_key = api_key) resp <- nectar::req_perform_opinionated(req) nectar::resp_parse(resp, response_parser = .fec_response_parser) } ``` Notice that the function includes API-key authentication arguments when appropriate! ### 020-auth.R Security for the API is defined in `R/020-auth.R`. The initial version of this file works, but you may want to edit the automatic output. In the case of the OpenFEC API, the description specifies three security schemes that overlap with one another: one that sets an `X-Api-Key` field in the header, and two that set an `api_key` in the query string. The names of the generated functions are based on the names of the security schemes in the OpenAPI document. ```{r fec_auth} # These functions were generated by the {beekeeper} package, based on # components@security_schemes from the source API description. You may want to # delete unused options. In addition, APIs often have additional security # options that are not formally documented in the API description. For example, # for any `location = query` `api_key` options, it might be possible to instead # pass the same parameter in a header, possibly with a different name. Consult # the text description of authentication in your API documentation. .fec_req_auth <- function(req, api_key = NULL) { if (!is.null(api_key)) { req <- .fec_req_auth_api_key_header_auth(req, api_key) req <- .fec_req_auth_api_key_query_auth(req, api_key) req <- .fec_req_auth_api_key(req, api_key) } return(req) } # An API key provided by the API provider. This key is not clearly documented in # the API description. Check the API documentation for details. .fec_req_auth_api_key_header_auth <- function(req, api_key) { nectar::req_auth_api_key( req, location = "header", parameter_name = "X-Api-Key", api_key = api_key ) } # An API key provided by the API provider. This key is not clearly documented in # the API description. Check the API documentation for details. .fec_req_auth_api_key_query_auth <- function(req, api_key) { nectar::req_auth_api_key( req, location = "query", parameter_name = "api_key", api_key = api_key ) } # An API key provided by the API provider. This key is not clearly documented in # the API description. Check the API documentation for details. .fec_req_auth_api_key <- function(req, api_key) { nectar::req_auth_api_key( req, location = "query", parameter_name = "api_key", api_key = api_key ) } ``` For the real package, I deleted the two `query` functions, since the `header` function is sufficient and slightly more secure. I also renamed the `header` function from `.fec_req_auth_api_key_header_auth` to `.fec_req_auth_api_key_header` to remove the redundant `_auth`. ### test files The generated package also includes tests for the API. If you have not already done so, it also activates the use of `{testthat}` in the package. To test the overall functionality of the package, provide an endpoint path in `tests/testthat/test-010-call.R`. A future version of `{beekeeper}` will attempt to auto-fill this path for you. ```{r test-010-call} httptest2::with_mock_dir("api/01-call/valid", { test_that("Can call an endpoint without errors", { # A path will be auto-filled in a future version of beekeeper. fail( "Provide any path for this API in PROVIDED_PATH, then delete this fail." ) PROVIDED_PATH <- "path/to/endpoint" expect_no_error(fec_call_api(PROVIDED_PATH)) }) }) ``` Manually edited to: ```{r test-010-call-fixed} httptest2::with_mock_dir("api/01-call/valid", { test_that("Can call an endpoint without errors", { PROVIDED_PATH <- "candidates" expect_no_error(fec_call_api(PROVIDED_PATH)) }) }) ``` You may also want to add specific tests for endpoints that require authentication vs endpoints that do not require authentication. A future version of `{beekeeper}` will attempt to auto-generate such tests when appropriate (when different paths use different security schemes).