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 (the “OAS”). Some APIs that follow the OAS can be found on APIs.guru. We will use the APIs.guru API (no authentication) and the OpenFEC API (authentication using an API key) as examples.
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.
If your target API follows the OAS, you can use your API’s OpenAPI Document to configure your package.
# 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"
)
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.
# 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 first file generated for the package is
R/010-call.R
. This file defines a function that can be used
to call the 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!
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.
# 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
.
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.
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:
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).