APIs II

Building APIs with Plumber + POST

Agenda

  1. Review GET requests
  2. Building an API
    • Schematic + Plumber
    • /greet endpoint
  3. Extending the API
    • /babynames endpoint
    • /plot endpoint
  4. POST requests
    • /grade endpoint

Review Get Requests

API Review

  • API: Application Programming Interface
  • Allows different software systems to communicate
  • Endpoints define specific functionalities
  • Parameters customize endpoint behavior
  • Can access via browser or programmatically
  • Returns data in structured format
  • Our reqests so far: GET (retrieve data)

Building an API

Plan for building an API

  1. Define endpoints
  2. Specify parameters
  3. Return structured responses

  1. Define endpoints
greet <- function(){
  greeting <- paste0("Hello!")
}

  1. Define endpoints
  2. Specify parameters
greet <- function(name = ""){
  greeting <- paste0("Hello ", name, "!")
}

  1. Define endpoints
  2. Specify parameters
  3. Return structured responses
greet <- function(name = ""){
  greeting <- paste0("Hello ", name, "!")
  list(greeting)
}

  1. Define endpoints
  2. Specify parameters
  3. Return structured responses
  4. Deploy as a web service

  1. Define endpoints
  2. Specify parameters
  3. Return structured responses
  4. Deploy as a web service Annotate with Plumber
greet <- function(name = ""){
  greeting <- paste0("Hello ", name, "!")
  list(greeting)
}

  1. Define endpoints
  2. Specify parameters
  3. Return structured responses
  4. ~Deploy as a web service~ Annotate with Plumber
#* @apiTitle Stat 133 API
#* @apiDescription A collection of simple example endpoints
#* 
#* Greet a person by name
#* @param name The person to greet
#* @get /greet
function(name = ""){
  greeting <- paste0("Hello ", name, "!")
  list(greeting)
}

Running a Plumber API from your session

  1. Move code into R script (e.g., stat133-api.R)
  2. Use plumber::plumb() to load the script
  3. Use pr_run() to start the API server
library(plumber)
stat133_api <- plumb("stat133-api.R")
pr_run(stat133_api,
       host = "0.0.0.0",
       port = 8080)

Connecting to the API

  • If your computer is the API server, you can access it at:
http://localhost:8080/greet?name=Andrew
  • If someone else wants to access your API, they need your IP address (which changes each time you connect to the internet) and be on the same network

Find your IP address at the terminal:

ipconfig getifaddr en0
  • For “production” use, deploy to a cloud service (e.g., Digital Ocean, AWS, Posit Connect, etc.)

Extending the API

#* Greet a person by name
#* @param name The person to greet
#* @get /greet
function(name = ""){
  greeting <- paste0("Hello ", name, "!")
  list(greeting)
}
library(babynames)
babynames
# A tibble: 1,924,665 × 5
    year sex   name          n   prop
   <dbl> <chr> <chr>     <int>  <dbl>
 1  1880 F     Mary       7065 0.0724
 2  1880 F     Anna       2604 0.0267
 3  1880 F     Emma       2003 0.0205
 4  1880 F     Elizabeth  1939 0.0199
 5  1880 F     Minnie     1746 0.0179
 6  1880 F     Margaret   1578 0.0162
 7  1880 F     Ida        1472 0.0151
 8  1880 F     Alice      1414 0.0145
 9  1880 F     Bertha     1320 0.0135
10  1880 F     Sarah      1288 0.0132
# ℹ 1,924,655 more rows

Question: Sketch ~7-9 lines of code to create a new endpoint /babynames that takes a param babyname and returns a data frame filtered on that name.

02:00

#* Get data for a name from the babynames dataset
#* @param babyname The name to filter on
#* @get /babynames
function(babyname = "Mary"){
  library(babynames)
  library(dplyr)

  myData <- babynames |>
    filter(name == babyname)
  
  myData
}

Returning Plots from an API

  • Plumber defaults to returning JSON
  • You can change the format of the returned data using serializers
  • Common serializers:
    • @serializer json (default)
    • @serializer png
    • @serializer jpeg
    • @serializer html
    • @serializer contentType list(type="image/svg+xml")

#* Plot timeseries of a name from the babynames dataset
#* @param babyname The name to plot (required)
#* @get /plot
#* @serializer png
function(babyname = "Mary"){
  library(babynames)
  library(dplyr)
  library(ggplot2)

  myData <- babynames |>
    filter(name == babyname) |>
    group_by(year) |>
    summarize(n = sum(n), .groups = "drop")

  p <- ggplot(myData, aes(x = year, y = n)) +
    geom_line() +
    labs(title = paste("Popularity of", babyname, "over time"),
         x = "year",
         y = "Count") +
    theme_bw()

  print(p)
}

Let’s improve the clarify by returning an SVG (support vector graphics) image instead of PNG.

#* Plot timeseries of a name from the babynames dataset
#* Returns an SVG image viewable directly in browser
#* @param babyname The name to plot (required)
#* @get /plot
#* @serializer contentType list(type="image/svg+xml")
function(babyname = "Mary"){
  library(babynames)
  library(dplyr)
  library(ggplot2)
  library(svglite)

  myData <- babynames |>
    filter(name == babyname) |>
    group_by(year) |>
    summarize(n = sum(n), .groups = "drop")

  p <- ggplot(myData, aes(x = year, y = n)) +
    geom_line() +
    labs(title = paste("Popularity of", babyname, "over time"),
         x = "year",
         y = "Count") +
    theme_bw()

  tf <- tempfile(fileext = ".svg")
  svglite::svglite(tf, width = 7, height = 5)
  print(p)
  dev.off()

  paste(readLines(tf, warn = FALSE), collapse = "\n")
}

POST Requests

What is a POST request?

  • Another HTTP method (like GET)
  • Used to send data to a server to create/update a resource or parameterize an operation
  • Data is not included in the URL (unlike GET)
  • Data is included in the body of the request
  • Commonly used for:
    • Submitting form data
    • Uploading files
    • Sending JSON payloads

#* Compute final course grade
#* @post /grade
function(req) {
  body <- jsonlite::fromJSON(req$postBody)
  
  ps  <- body$problem_sets
  prj <- body$projects
  qz  <- body$quizzes
  fn  <- body$final
  
  course_grade <- 
    0.05 * ps +
    0.15 * prj +
    0.50 * qz +
    0.30 * fn
  
  list(course_grade)
}

Making a POST request

library(httr2)

my_grades <- list(problem_sets = 92,
                  projects = 88,
                  quizzes = 81,
                  final = 90)

resp <- request("http://localhost:8080/grade") |>
  req_method("POST") |>
  req_body_json(my_grades) |>
  req_perform()

resp |> 
  resp_body_json()