API development with R

Roberto Villegas-Diaz

University of Liverpool

Outline

  • Day 1
    • Introduction to APIs
    • Working with APIs
    • Introduction to {plumber}
  • Day 2
    • Building your own API
    • Further considerations

Introduction to APIs

What’s an API?

Application Programming Interface

Source: (Tolassy, 2015)

Why use APIs?

  • Streamline your data flows
  • Common language
  • Static interface
  • Enable others to use your “products”

How do APIs work?

You don’t need to know how the kitchen or the restaurant operates, you just need to know how to order. (Grgurić and Buhler, 2020)

Source: (Layne, 2020)

Example APIs

Few examples (there are too many):


Source: (Fox, 2016)

Example APIs

Few examples (there are too many):

Resources of interest:

Working with APIs

Structure of a Query

Source: (realisable, 2023)

Structure of a Query

Source: (realisable, 2023)

“Method” “Scheme” “Server” “Path” “Query String”
Pavement Liver building Flat 3 Kitchen/cupboard/get_mug colour=red
Road Liver building Garage park_car

Structure of a Query: Response

Structure of a Query: Example

Try running the following line of code:

url("https://api.crossref.org/works/<DOI>")

Replace <DOI> by any DOI (Direct Object Identifier) you want! For example: 10.1016/j.gloplacha.2022.103790.

Structure of a Query: Example

Try running the following line of code:

url("https://api.crossref.org/works/10.1016/j.gloplacha.2022.103790")
A connection with                                                                            
description "https://api.crossref.org/works/10.1016/j.gloplacha.2022.103790"
class       "url-libcurl"                                                   
mode        "r"                                                             
text        "text"                                                          
opened      "closed"                                                        
can read    "yes"                                                           
can write   "no"                                                            

Structure of a Query: Example

Try running the following line of code:

con <- url("https://api.crossref.org/works/10.1016/j.gloplacha.2022.103790")
tmp <- readLines(con)
strsplit(tmp, ",")[[1]][1:12] # extract few rows
 [1] "{\"status\":\"ok\""                             
 [2] "\"message-type\":\"work\""                      
 [3] "\"message-version\":\"1.0.0\""                  
 [4] "\"message\":{\"indexed\":{\"date-parts\":[[2024"
 [5] "2"                                              
 [6] "7]]"                                            
 [7] "\"date-time\":\"2024-02-07T17:44:10Z\""         
 [8] "\"timestamp\":1707327850108}"                   
 [9] "\"reference-count\":52"                         
[10] "\"publisher\":\"Elsevier BV\""                  
[11] "\"license\":[{\"start\":{\"date-parts\":[[2022" 
[12] "4"                                              

Structure of a Query: Example

A nicer approach to run this code:

result <- jsonlite::read_json(
  "https://api.crossref.org/works/10.1016/j.gloplacha.2022.103790"
)

# get the query status
result$status
[1] "ok"
# get field from the result, 'reference count'
result$message$`reference-count`
[1] 52

Hands on session 1

  • [Guided] How many people are on the International Space Station (ISS) right now?

  • [DIY] What is the current position of the International Space Station? Can you create a plot?

Note

Could you please download the following zip file? This file contains all the scripts we will use for the hands-on sessions.

http://tinyurl.com/xxiv-simmac-api

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

response <- httr::GET("http://api.open-notify.org/astros.json")
response

Note

Note the values for Status and Content-Type! Both useful to determine whether the query was successful and the type of content returned.

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

response <- httr::GET("http://api.open-notify.org/astros.json")

# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))

# check number of people
content$number

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

response <- httr::GET("http://api.open-notify.org/astros.json")

# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))

# check the people
content$people
                name craft
1    Jasmin Moghbeli   ISS
2   Andreas Mogensen   ISS
3   Satoshi Furukawa   ISS
4 Konstantin Borisov   ISS
5     Oleg Kononenko   ISS
6       Nikolai Chub   ISS
7       Loral O'Hara   ISS

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

Alternative

Note that we can use the API call with the jsonlite::read_json function; however, we don’t get as much details from the call as we did with our previous approach. Also, this only works when the response returned is in JSON format.

# read contents from API call
contents <- jsonlite::read_json("http://api.open-notify.org/astros.json")

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

# read contents from API call
contents <- jsonlite::read_json("http://api.open-notify.org/astros.json")

# check status of query
contents$message
[1] "success"

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

# read contents from API call
contents <- jsonlite::read_json("http://api.open-notify.org/astros.json")

# check number of people
contents$number
[1] 7

[Guided] How many people are on the International Space Station right now?

API call: http://api.open-notify.org/astros.json

# read contents from API call
contents <- jsonlite::read_json("http://api.open-notify.org/astros.json")

# extract details of the crew
contents$people |>
  purrr::map(\(x) tibble::tibble(name = x$name, craft = x$craft)) |>
  purrr::list_rbind()
# A tibble: 7 × 2
  name               craft
  <chr>              <chr>
1 Jasmin Moghbeli    ISS  
2 Andreas Mogensen   ISS  
3 Satoshi Furukawa   ISS  
4 Konstantin Borisov ISS  
5 Oleg Kononenko     ISS  
6 Nikolai Chub       ISS  
7 Loral O'Hara       ISS  

[DIY] What is the current position of the International Space Station? Can you create a plot?

API call: http://api.open-notify.org/iss-now.json

response <- httr::GET("http://api.open-notify.org/iss-now.json")

# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))

# get timestamp
as.POSIXct(content$timestamp, origin = "1970-01-01")
[1] "2024-02-22 22:15:32 GMT"
# get position coordinates
content$iss_position
$latitude
[1] "-50.1975"

$longitude
[1] "-31.5457"

[DIY] What is the current position of the International Space Station? Can you create a plot?

API call: http://api.open-notify.org/iss-now.json

We can create a helper function to extract multiple records:

iss_position <- function() {
  response <- httr::GET("http://api.open-notify.org/iss-now.json")
  # the content is in binary, so convert the response to an R data object
  content <- jsonlite::fromJSON(rawToChar(response$content))
  Sys.sleep(1) # pause for 1 second
  # extract each field and convert to the appropriate data type
  tibble::tibble(
    timestamp = as.POSIXct(content$timestamp, origin = "1970-01-01"),
    longitude = as.numeric(content$iss_position$longitude),
    latitude = as.numeric(content$iss_position$latitude)
  )
}

# map over the helper function X times, here 10
iss_position_tbl <- seq_len(10) |> # number of positions to extract
  purrr::map(\(x) iss_position()) |>
  purrr::list_rbind()

[DIY] What is the current position of the International Space Station? Can you create a plot?

Here is a subset of the position of the ISS, capture over a period of 15 minutes, seq_len(15 * 60):

timestamp longitude latitude
2024-02-18 20:17:19 -94.8448 11.1795
2024-02-18 20:17:20 -94.8072 11.1294
2024-02-18 20:17:21 -94.7697 11.0792
2024-02-18 20:17:22 -94.7134 11.0040
2024-02-18 20:17:23 -94.6759 10.9538
2024-02-18 20:17:24 -94.6385 10.9036
2024-02-18 20:17:26 -94.5823 10.8283
2024-02-18 20:17:27 -94.5448 10.7781
2024-02-18 20:17:28 -94.5074 10.7279
2024-02-18 20:17:29 -94.4513 10.6526
2024-02-18 20:34:45 -50.5240 -38.9132
2024-02-18 20:34:46 -50.4619 -38.9522
2024-02-18 20:34:47 -50.3997 -38.9911
2024-02-18 20:34:48 -50.3063 -39.0495
2024-02-18 20:34:49 -50.2439 -39.0883
2024-02-18 20:34:50 -50.1814 -39.1271
2024-02-18 20:34:52 -50.0876 -39.1852
2024-02-18 20:34:53 -50.0250 -39.2239
2024-02-18 20:34:54 -49.9623 -39.2625
2024-02-18 20:34:55 -49.8681 -39.3205

Note

Note that only the top 10 rows and the bottom 10 rows are displayed, there are an additional 880 rows of data ranging from 2024-02-18 20:17:30 to 2024-02-18 20:34:45.

[DIY] What is the current position of the International Space Station? Can you create a plot?

Plot the position of the ISS, here using the {leaflet} package:

# create icon from online image
iss_icon <- leaflet::makeIcon(
  iconUrl = "https://cdn-icons-png.flaticon.com/512/81/81959.png", 
  iconWidth = 15, 
  iconHeight = 15
)

# create plot of the positions
iss_position_tbl |>
  leaflet::leaflet() |>
  leaflet::addTiles() |>
  leaflet::addMarkers(
    lng = ~longitude, 
    lat = ~latitude, 
    label = ~timestamp,
    icon = iss_icon
  )

[DIY] What is the current position of the International Space Station? Can you create a plot?

Questions? / Short break



Comic source: https://xkcd.com/2191

Introduction to {plumber}



What’s {plumber}?

Plumber allows you to create a web API by merely decorating your existing R source code with roxygen2-like comments. (Schloerke and Allen, 2022)

R comments & decorators

  • Regular R comments are included with the #.
  • Roxygen2 comments allow the user to document their functions with the notation #' which is translated into R documentation.
  • {plumber} uses the notation #*.

{plumber}-ising a function / notation

Given a simple hello world function

# This function returns a message
hello_world <- function() {
  return("Hello XXIV SIMMAC!")
}

{plumber}-ising a function / notation

Given a simple hello world function:

#* This function returns a message
#* @get /hello_world
function() {
  return("Hello XXIV SIMMAC!")
}

{plumber}-ised function!

Note

  • The change from # to #* for the comments.
  • The addition of @get /<function_name>.
  • The function name was relocated.

{plumber}-ising a function / notation: with params

Given a function to calculate the square of a number, a:

# This function calculates the square of `a`
square <- function(a) {
  return(as.numeric(a) ^ 2)
}

{plumber}-ising a function / notation: with params

Given a function to calculate the square of a number, a:

#* This function calculates the square of `a`
#* @param a Numeric value.
#* @get /square 
function(a) {
  return(as.numeric(a) ^ 2)
}

{plumber}-ised function!

Note

  • The change from # to #* for the comments.
  • The addition of @param <param_name> <description. If the function had multiple params, the each of them will have to be documented using this format.
  • The addition of @get /<function_name>.
  • The function name was relocated.

Routing & Input

An incoming HTTP request must be “routed” to one or more R functions. Plumber has two distinct families of functions that it handles: endpoints and filters. (Schloerke and Allen, 2022)

Routing & Input: endpoints

An endpoint is an annotated function, like those we already saw:

#* This function returns a message
#* @get /hello_world
function() {
  return("Hello XXIV SIMMAC!")
}

Note

This annotation specifies that this function is responsible for generating the response to any GET request to /hello_world.

Routing & Input: endpoints

The annotations that generate an endpoint include:

  • @get: Read
  • @post: Read / write
  • @put: Update / replace
  • @delete: Delete

An endpoint can support multiple methods:

#* @get /cars
#* @post /cars
#* @put /cars
function(){
  ...
}

Routing & Input: filters

allow to break down complex logic into a sequence of independent, understandable steps. (Schloerke and Allen, 2022)

Filters can do one of three things in handling a request:

  • Forward control onto the next handler, potentially after mutating the request.
  • Return a response itself and not forward to subsequent handlers
  • Throw an error

Routing & Input: filters

allow to break down complex logic into a sequence of independent, understandable steps. (Schloerke and Allen, 2022)

#* @filter removeSemicolon
function(req, res) {
  req$args <- req$args |>
    purrr::map(stringr::str_remove_all, pattern = ";")
  plumber::forward()
}

Note

  • Note the inclusion of the @filter decorator.

  • A filter has two parameters, req (request) and res (result).

    • Both req and res are R environments, which means that changes done by a filter, will be visible to subsequent filters/endpoints.
  • The plumber::forward() call, passes control to the next handler in the pipeline (another filter or an endpoint. (Schloerke and Allen, 2022)

Rendering Output

A response object (an environment) contains the following elements:

Name Example Description
headers list(header = "abc") A list of HTTP headers to include in the response
body NULL This is set to the serialized output of the handler
status 200 The HTTP status code included in the response

Rendering Output: serializers

In order to send a response from R to an API client, the object must be “serialized” into some format that the client can understand. (Schloerke and Allen, 2022)

Some examples:

  • CSV: @serializer csv
  • JPEG: @serializer jpeg
  • JSON: @serializer json
  • PDF: @serializer pdf
  • PNG: @serializer png
  • Text: @serializer text

Rendering Output: serializers

Serializers can also be customised:

#* Example of custom graphical output
#* @serializer png list(width = 400, height = 500)
#* @get /
function(){
  plot(1:10)
}

Error handling:

#* Example of throwing an error
#* @get /simple
function() {
  stop("I'm an error!")
}

#* Generate a friendly error
#* @get /friendly
function(res) {
  msg <- "Your request did not include a required parameter."
  res$status <- 400 # Bad request
  list(error = jsonlite::unbox(msg))
}

Building your own API

Building your own API

Warning

Before you attempt the following steps, you must execute the following command:

install.packages("plumber")
  1. Inside RStudio, click on File > New File > Plumber API...:

Building your own API

  1. Next, choose a name for your API (e.g., XXIV_SIMMAC) and click on Create:

Building your own API

  1. A new R script (called plumber.R) should be displayed in your RStudio session.
# This is a Plumber API. You can run the API by clicking
# the 'Run API' button above.
#
# Find out more about building APIs with Plumber here:
#
#    https://www.rplumber.io/
#
library(plumber)

#* @apiTitle Plumber Example API
#* @apiDescription Plumber example description.

#* Echo back the input
#* @param msg The message to echo
#* @get /echo
function(msg = "") {
    list(msg = paste0("The message is: '", msg, "'"))
}

Note that a new button should have appeared at the top of your script options:

Clicking on this button will deploy your API locally.

Hands on session 2

I. Write a plumber function to use the Gapminder dataset to find the population of Costa Rica in 1982.

Note

Gapminder is a dataset of populations of various countries from 1952 - 2007. We will access it with the {gapminder} package. (Bryan, 2023)

  1. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

  2. Write a plumber function to plot the population change of a user defined country.

I. Write a plumber function to use the Gapminder dataset to find the population of Costa Rica in 1982.

Where do we start?

  1. We can start by creating a “simple” R function that gets the data we want!
# This function returns the population of Costa Rica in 1982
pop_cr_1982 <- function() {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == "Costa Rica") |>
    dplyr::filter(year == 1982)
  return(pop_tbl$pop)
}

# call our function
pop_cr_1982()
[1] 2424367

I. Write a plumber function to use the Gapminder dataset to find the population of Costa Rica in 1982.

  1. Next, we can add the “{plumber} decorators” to transform this function.
#* This function returns the population of Costa Rica in 1982
#* @get /pop_cr_1982 
function() {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == "Costa Rica") |>
    dplyr::filter(year == 1982)
  return(pop_tbl$pop)
}

I. Write a plumber function to use the Gapminder dataset to find the population of Costa Rica in 1982.

  1. After deploying the API, you can test it as you already learnt:
response <- httr::GET("http://127.0.0.1:1234/pop_cr_1982")
response
Response [http://127.0.0.1:1234/pop_cr_1982]
  Date: 2024-02-22 22:15
  Status: 200
  Content-Type: application/json
  Size: 9 B
# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))
content
[1] 2424367

II. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

Where do we start?

  1. We can start by adapting our previous solution.
#* This function returns the population of Costa Rica in 1982
#* @get /pop_cr_1982 
function() {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == "Costa Rica") |>
    dplyr::filter(year == 1982)
  return(pop_tbl$pop)
}

II. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

Where do we start?

  1. We can start by adapting our previous solution.
#* This function returns the population for a `country` in a particular `year`
#* @param country String with a country in the Gapminder dataset: https://doi.org/10.7910/DVN/GJQNEQ.
#* @param year A numeric value with a year of data available in Gapminder dataset (1952 - 2007 in steps of 5 years).
#* @get /pop_country
function(country, year) {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == !!country) |>
    dplyr::filter(year == as.numeric(!!year))
  return(pop_tbl$pop)
}

Note

Note the addition of the bang-bang operator, to “unquote” the values received for country and year. (Wickham, 2014)

II. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

  1. After deploying the API, you can test it as you already learnt:
# encode URL / replace spaces, etc.
url <- URLencode("http://127.0.0.1:1234/pop_country?country=United Kingdom&year=1982")
response <- httr::GET(url)
response
Response [http://127.0.0.1:1234/pop_country?country=United%20Kingdom&year=1982]
  Date: 2024-02-22 22:15
  Status: 200
  Content-Type: application/json
  Size: 10 B
# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))
content
[1] 56339704

Note

Note that this call to the API has an extra line,

URLencode("http://127.0.0.1:1234/pop_country?country=United Kingdom&year=1982")

The function URLencode changes special characters (like spaces) into the appropriate hexadecimal representation.

II. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

Further improvements, the solution shown before does not handle (in a user friendly) common errors like:

  • entering a country that doesn’t exist in the Gapminder data
  • choosing a year outside those available
  • choosing a non-numeric value for the year
  • and probably others.

II. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

Is there anything we can do to validate these fields?

Yes, of course! We can create a filter or perform these checks within the endpoint.

#* This filter checks for valid countries in the Gapminder dataset
#* @filter validate_year
function(req, res) {
  # check if the calling endpoint has a year parameter
  if ("year" %in% names(req$args)) {
    # check if the given year is in the Gapminder dataset
    year <- as.numeric(req$args$year)
    gapminder_years <- unique(gapminder::gapminder$year)
    # do check
    status <- any(year %in% gapminder_years)
    if (!status) {
      msg <- paste0("The given year, ", year, 
                    ", it's not part of the Gapminder dataset. ",
                    "Please, try one of the following: ",
                    paste0(gapminder_years, collapse = ", ")) 
      res$status <- 400 # Bad request
      return(list(
        error = jsonlite::unbox(msg),
        valid_years = gapminder_years
        )
      )
    }
  }
  plumber::forward()
}

II. Write a plumber function to allow a user to find out the population of any country during any year in the Gapminder dataset.

Let’s test if the new filter works, by requesting data for a year not in the Gapminder dataset:

# encode URL / replace spaces, etc.
url <- URLencode("http://127.0.0.1:1234/pop_country?country=United Kingdom&year=2024")
response <- httr::GET(url)
response
Response [http://127.0.0.1:1234/pop_country?country=United%20Kingdom&year=2024]
  Date: 2024-02-22 22:15
  Status: 400
  Content-Type: application/json
  Size: 254 B
# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))
content
$error
[1] "The given year, 2024, it's not part of the Gapminder dataset. Please, try one of the following: 1952, 1957, 1962, 1967, 1972, 1977, 1982, 1987, 1992, 1997, 2002, 2007"

$valid_years
 [1] 1952 1957 1962 1967 1972 1977 1982 1987 1992 1997 2002 2007

III. Write a plumber function to plot the population change of a user defined country.

Where do we start?

  1. We can start by adapting our previous solution.
#* This function returns the population for a `country` in a particular `year`
#* @param country String with a country in the Gapminder dataset: https://doi.org/10.7910/DVN/GJQNEQ.
#* @param year A numeric value with a year of data available in Gapminder dataset (1952 - 2007 in steps of 5 years).
#* @get /pop_country
function(country, year) {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == !!country) |>
    dplyr::filter(year == as.numeric(!!year))
  return(pop_tbl$pop)
}

III. Write a plumber function to plot the population change of a user defined country.

Where do we start?

  1. We can start by adapting our previous solution.
#* This function returns a plot of the change in population for a `country`, as per the Gapminder dataset
#* @param country String with a country in the Gapminder dataset: https://doi.org/10.7910/DVN/GJQNEQ.
#* @serializer png
#* @get /pop_country_change
function(country) {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == !!country) |>
    dplyr::select(year, pop)
  
  options(scipen=999) # Change number format on axes
  plot(pop_tbl, xlab = "Year", ylab = "Population")
}

III. Write a plumber function to plot the population change of a user defined country.

  1. After deploying the API, you can test it as you already learnt:
# encode URL / replace spaces, etc.
url <- URLencode("http://127.0.0.1:1234/pop_country_change?country=Costa Rica")
response <- httr::GET(url)
response
Response [http://127.0.0.1:1234/pop_country_change?country=Costa%20Rica]
  Date: 2024-02-22 22:15
  Status: 200
  Content-Type: image/png
  Size: 17.3 kB
<BINARY BODY>
# the content is in binary, so convert the response to an R data object
content <- png::readPNG(response$content)
grid::grid.raster(content, height = grid::unit(5, "in"))

III. Write a plumber function to plot the population change of a user defined country.

  1. Alternative, instead of returning a plot, we can return a data frame by updating the endpoint:
#* This function returns a data frame with the change in population for a `country`, as per the Gapminder dataset
#* @param country String with a country in the Gapminder dataset: https://doi.org/10.7910/DVN/GJQNEQ.
#* @get /pop_country_change_df
function(country) {
  pop_tbl <- gapminder::gapminder |>
    dplyr::filter(country == !!country) |>
    dplyr::select(year, pop)
  return(pop_tbl)
}

III. Write a plumber function to plot the population change of a user defined country.

# encode URL / replace spaces, etc.
url <- URLencode("http://127.0.0.1:1234/pop_country_change_df?country=Costa Rica")
response <- httr::GET(url)
response
Response [http://127.0.0.1:1234/pop_country_change_df?country=Costa%20Rica]
  Date: 2024-02-22 22:15
  Status: 200
  Content-Type: application/json
  Size: 336 B
# the content is in binary, so convert the response to an R data object
content <- jsonlite::fromJSON(rawToChar(response$content))
tibble::as_tibble(content)
# A tibble: 12 × 2
    year     pop
   <int>   <int>
 1  1952  926317
 2  1957 1112300
 3  1962 1345187
 4  1967 1588717
 5  1972 1834796
 6  1977 2108457
 7  1982 2424367
 8  1987 2799811
 9  1992 3173216
10  1997 3518107
11  2002 3834934
12  2007 4133884

III. Write a plumber function to plot the population change of a user defined country.

  1. Alternative plot:
response <- httr::GET(URLencode("http://127.0.0.1:1234/pop_country_change_df?country=Costa Rica"))
jsonlite::fromJSON(rawToChar(response$content)) |>
  tibble::as_tibble() |>
  ggplot2::ggplot() +
  ggplot2::geom_point(ggplot2::aes(x = year, y = pop / 1E6)) +
  ggplot2::labs(x = "Year", y = "Population [millions]", title = "Population change of Costa Rica") +
  ggplot2::theme_bw()

Further considerations

Further considerations

Thank you!

r.villegas-diaz@liverpool.ac.uk

Acknowledgements

In association with

References

Bryan, J., 2023. Gapminder: Data from gapminder.
Butters, O., 2022. Health data science: Plumber lecture. University of Liverpool.
Fox, T., 2016. Anatomy of an HTTP request & response. Trevor Fox.
Grgurić, E., Buhler, J., 2020. Introduction to APIs. The University of British Columbia.
Layne, C., 2020. A is for application: API basics. Medium.
Parker, A., n.d. What are APIs and how do they work? tray.io.
realisable, 2023. Anatomy of an HTTP request & response. realisable.
Schloerke, B., Allen, J., 2022. Plumber: An API generator for R.
Teplitzky, S., Tranfield, W., Warren, M., White, P., 2021. Introducing reproducibility to citation analysis: A case study in the earth sciences. Journal of eScience Librarianship 10.
Tolassy, K., 2015. Apis: Introduction and context. Mobapi project.
Wickham, H., 2014. Advanced r, Chapman & hall/CRC the r series. Taylor & Francis.
Zhong, H., Mei, H., 2019. An empirical study on API usages. IEEE Transactions on Software Engineering 45, 319–334.