Skip to contents

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.

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.

# 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.

# 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.

# 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.

# 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.

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).