Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

First plumber chapter. #14

Merged
merged 1 commit into from
Nov 17, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions httr2-authentication.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,7 @@
- Goal: Make the key as short-lived as possible
- Not everyone implements the same way (ie often wrong)
- `httr2::req_oauth_*()`
- **Other:** [Certificates](https://fosstodon.org/deck/@jaredlander/111422477967658857), other?

## Dangers of leaking credentials {-}

Expand Down
Binary file added images/plumber-intro_echo.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added images/plumber-intro_plot.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
183 changes: 178 additions & 5 deletions plumber-intro.Rmd
Original file line number Diff line number Diff line change
@@ -1,13 +1,186 @@
# How can I create my own APIs?
# How can I create an API?

**Learning objectives:**

- THESE ARE NICE TO HAVE BUT NOT ABSOLUTELY NECESSARY
- Create an API with {plumber}.
- Organize {plumber} API projects.
- Design a RESTful API.
- Implement a RESTful API in {plumber}.
- Use {plumber} filters to process an API request.
- Use {plumber} hooks to modify an API.
- Debug a {plumber} API.

## SLIDE 1 {-}
## plumber quickstart {-}


1. Define API endpoints in an R script.
2. Launch that API locally.
3. *Later chapter(s): Deploy that API somewhere else.*

## echo endpoint: function {-}

```{r plumber-intro-echo1, eval = TRUE}
echo <- function(msg = "") {
list(msg = paste0("The message is: '", msg, "'"))
}

echo("my message")
```

## echo endpoint: plumber {-}

```{r plumber-intro-echo2, eval = FALSE}
#* Echo back the input parameter
#* @param msg The message to echo back.
#* @get /echo
function(msg = "") {
list(msg = paste0("The message is: '", msg, "'"))
}
```

## echo endpoint: plumber (alternate) {-}

```{r plumber-intro-echo2b, eval = FALSE}
#* Echo back the input parameter
#* @param msg The message to echo back.
#* @get /echo
echo <- function(msg = "") {
list(msg = paste0("The message is: '", msg, "'"))
}
```

or

```{r plumber-intro-echo2c, eval = FALSE}
#* Echo back the input parameter
#* @param msg The message to echo back.
#* @get /echo
echo # Defined elsewhere
```

## echo endpoint in action {-}

![](images/plumber-intro_echo.png)

## plot endpoint {-}

```{r plumber-intro-plot, eval = FALSE}
#* Plot out data from the iris dataset
#* @param spec If provided, filter the data to only this species (e.g. 'setosa')
#* @get /plot
#* @serializer png
function(spec) {
myData <- iris
title <- "All Species"

# Filter if the species was specified
if (!missing(spec)){
title <- paste0("Only the '", spec, "' Species")
myData <- subset(iris, Species == spec)
}

plot(myData$Sepal.Length, myData$Petal.Length,
main=title, xlab="Sepal Length", ylab="Petal Length")
}
```

## plot endpoint in action {-}

![](images/plumber-intro_plot.png)

## Launch the API {-}

- `api <- pr("plumber.R")`
- `api <- pr() |> pr_post({definition}) |> etc()`
- `api <- plumb(dir = "path/to/definition")`
- Looks for `entrypoint.R`, else `plumber.R`

In any case: `pr_run(api)` to run locally.

## Organize plumber projects {-}

- Non-Package:
- `plumber.R` = "main" file
- Can `source()` any other files, but
- `#*` defs must be in main file
- Alternative: `entrypoint.R` file to construct `api` programmatically
- Package:
- `inst/plumber/{API_DIR_NAME}/plumber.R`
- Same rules as above, but filenames matter more
- Least surprise = use `entrypoint.R`
- `plumb_api(package = "{pkg}", name = "{API_DIR_NAME}")`
- `available_apis(package = "{pkg}")`

## Nest plumber APIs {-}

```{r plumber-intro-mount, eval = FALSE}
# Define in entrypoint.R

users <- pr("users.R")
products <- pr("products.R")

pr |>
pr_mount("/users", users) |>
pr_mount("/products", products)
```


## Design a RESTful API {-}

- Endpoints = ***nouns***
- Subpaths = particular individual
- `/tasks` = all tasks, vs
- `/tasks/1234` = task ID 1234
- Methods define action
- `GET` = fetch
- `POST` = create
- `PUT` = replace
- `PATCH` = modify
- `DELETE` = remove

## More on designing APIs {-}

- Many books on just this
- [Designing APIs with Swagger and OpenAPI](https://livebook.manning.com/book/designing-apis-with-swagger-and-openapi) by Joshua S. Ponelat & Lukas L. Rosenstock
- (more suggestions to come)

## Implementing APIs in plumber {-}

- Generally think of each method as a separate thing
- `#* @get /tasks` totally separate block from `#* @post /tasks`
- *Can* put multiple `@method`s in 1 block
- Function can use `req$REQUEST_METHOD` for routing
- Remember `pr_mount()` for nested APIs!

## Process requests with filters {-}

- Filter process before endpoints
- `#* @filter FILTER_NAME`
- Do one of 3 things at end:
- Forward control to next handler
- Return a response itself without forwarding to endpoint
- Throw an error
- Inputs = `req` (the request object), `res` (the response object)
- More on these in next chapter


## Modify APIs with hooks {-}

- Execute code at points in request lifecycle
- `preroute(data, req, res)`
- `postroute(data, req, res, value)`
- `preserialize(data, req, res, value)`
- `postserialize(data, req, res, value)`
- `pr_hook()` for 1, `pr_hooks()` for multiple
- Use for: logging, open/close DB connection, debugging

## Debug a plumber API {-}

- `print()`, `cat()`, `cli::cli_inform()` in code to throw info to console
- Use hooks to log things along the path
- `browser()` in API functions will throw RStudio into debugger
- `pr_set_debug()` is ***on*** by default when interactive

- ADD SLIDES AS SECTIONS (`##`).
- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF.

## Meeting Videos {-}

Expand Down
8 changes: 8 additions & 0 deletions plumber-io.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
- ADD SLIDES AS SECTIONS (`##`).
- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF.

## random notes {-}

Include ideas from these [plumber](https://www.rplumber.io/) articles:

- [Routing & Input](https://www.rplumber.io/articles/routing-and-input.html)
- Specifically input bits. Also Dynammic Routes
- [Rendering Output](https://www.rplumber.io/articles/rendering-output.html)

## Meeting Videos {-}

### Cohort 1 {-}
Expand Down
6 changes: 6 additions & 0 deletions plumber-security.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
- ADD SLIDES AS SECTIONS (`##`).
- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF.

## random notes {-}

Include ideas from these [plumber](https://www.rplumber.io/) articles:

- [Security](https://www.rplumber.io/articles/security.html)

## Meeting Videos {-}

### Cohort 1 {-}
Expand Down
8 changes: 8 additions & 0 deletions plumber-test.Rmd
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,14 @@
- ADD SLIDES AS SECTIONS (`##`).
- TRY TO KEEP THEM RELATIVELY SLIDE-LIKE; THESE ARE NOTES, NOT THE BOOK ITSELF.

## random notes {-}

Include ideas from these [plumber](https://www.rplumber.io/) articles:

- [Runtime](https://www.rplumber.io/articles/execution-model.html)

[JumpingRivers had a blog about this!](https://www.jumpingrivers.com/blog/api-as-a-package-testing/)

## Meeting Videos {-}

### Cohort 1 {-}
Expand Down