forked from rstudio/apis-plumber
-
Notifications
You must be signed in to change notification settings - Fork 0
/
08-programmatic-usage.Rmd
131 lines (89 loc) · 6.49 KB
/
08-programmatic-usage.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
# Programmatic Usage {#programmatic-usage}
Typically, users define APIs using the "annotations," or special comments in their API source code. It is possible to define a Plumber API programmatically using the same underlying R6 objects that get created automatically when you define your API using annotations. Interacting with Plumber at this level can offer more control and specificity about how you want your API to behave.
## Creating and Controlling a Router
The centerpiece of Plumber is the router. Plumber routers are responsible for coordinating incoming requests from httpuv, dispatching the requests to the appropriate filters/endpoints, serializing the response, and handling errors that might pop up along the way. If you've been using annotations to define Plumber APIs, then you've already worked with Plumber routers as that's what the `plumb()` command produces.
To instantiate a new Plumber router programmatically, you can call `plumber$new()`. This will return a blank Plumber router with no endpoints. You could call `run()` on the returned object to start the API, but it doesn't know how to respond to any requests so any incoming traffic would get a `404` response. We'll see momentarily how to add endpoints and filters onto this empty router. Alternatively, you can pass a file that contains your annotation-based Plumber API as the first parameter to create a router much like you do with `plumb()`.
Be aware that Plumber routers do come with a handful of filters pre-configured. These built-in filters are used to do things like process properties of the incoming request like its cookies, `POST` body, or query string. You can specify which filters you want in your new router by overriding the `filters` parameter when creating your new router.
## Defining Endpoints
You can define endpoints on your router by using the `handle()` method. For instance, to define a Plumber API that response to `GET` requests on `/` and `POST` requests on `/submit`, you could use the following code:
```r
pr <- plumber$new()
pr$handle("GET", "/", function(req, res){
# ...
})
pr$handle("POST", "/submit", function(req, res){
# ...
})
```
The "handler" functions that you define in these `handle` calls are identical to the code you would have defined in your `plumber.R` file if you were using annotations to define your API.
The `handle()` method takes additional arguments that allow you to control nuanced behavior of the endpoint like which filter it might preempt or which serializer it should use. For instance, the following endpoint would use Plumber's HTML serializer.
```r
pr <- plumber$new()
pr$handle("GET", "/", function(){
"<html><h1>Programmatic Plumber!</h1></html>"
}, serializer=plumber::serializer_html())
```
## Defining Filters
Use the `filter()` method of a Plumber router to define a new filter:
```r
pr <- plumber$new()
pr$filter("myFilter", function(req){
req$filtered <- TRUE
forward()
})
pr$handle("GET", "/", function(req){
paste("Am I filtered?", req$filtered)
})
```
You can specify other options such as the serializer to use if the filter returns a value in the `filter()` method, as well.
## Registering Hooks on a Router {#router-hooks}
Plumber routers support the notion of "hooks" that can be registered to execute some code at a particular point in the lifecycle of a request. Plumber routers currently support four hooks:
- `preroute(data, req, res)`
- `postroute(data, req, res, value)`
- `preserialize(data, req, res, value)`
- `postserialize(data, req, res, value)`
In all of the above you have access to a disposable environment in the `data` parameter that is created as a temporary data store for each request. Hooks can store temporary data in these hooks that can be reused by other hooks processing this same request.
One feature when defining hooks in Plumber routers is the ability to modify the returned value. The convention for such hooks is: any function that accepts a parameter named `value` is expected to return the new value. This could be an unmodified version of the value that was passed in, or it could be a mutated value. But in either case, if your hook accepts a parameter named `value`, whatever your hook returns will be used as the new value for the response.
You can add hooks using the `registerHook` method, or you can add multiple hooks at once using the `registerHooks` method which takes a name list in which the names are the names of the hooks, and the values are the handlers themselves.
```r
pr <- plumber$new()
pr$registerHook("preroute", function(req){
cat("Routing a request for", req$PATH_INFO, "...\n")
})
pr$registerHooks(list(
preserialize=function(req, value){
print("About to serialize this value:")
print(value)
# Must return the value since we took one in. Here we're not choosing
# to mutate it, but we could.
value
},
postserialize=function(res){
print("We serialized the value as:")
print(res$body)
}
))
pr$handle("GET", "/", function(){ 123 })
```
Making a `GET` request to `/` will print out various information from the three events for which we registered hooks.
## Mounting & Static File Routers {#mount-static}
Plumber routers can be "nested" by mounting one into another using the `mount()` method. This allows you to compartmentalize your API by paths which is a great technique for decomposing large APIs into smaller files.
```
root <- plumber$new()
users <- plumber$new("users.R")
root$mount("/users", users)
products <- plumber$new("products.R")
root$mount("/products", products)
```
This is the same approach used for defining routers that serve a directory of static files. Static file routers are just a special case of Plumber routers created using `PlumberStatic$new()`. For example
```r
pr <- plumber$new()
stat <- PlumberStatic$new("./myfiles")
pr$mount("/assets", stat)
```
This will make the files and directories stored in the `./myfiles` directory available on your API under the `/assets/` path.
## Customizing a Router {#customize-router}
There are a handful of useful methods to be aware of to modify the behavior of a router. [Using hooks to alter request processing](#router-hooks) has already been discussed, but additionally you can modify a router's behavior using any of the following:
- `setSerializer()` - Sets the default serializer of the router.
- `setErrorHandler()` - Sets the error handler which gets invoked if any filter or endpoint generates an error.
- `set404Handler()` - Sets the handler that gets called if an incoming request can't be served by any filter, endpoint, or sub-router.