Skip to content

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 pr(). This will return a blank Plumber router with no endpoints. You could call pr_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 pr_handle(), pr_get(), or pr_post(). For instance, to define a Plumber API that response to GET requests on / and POST requests on /submit, you could use the following code:

pr() %>%
  pr_get("/", function(req, res){
    # ...
  }) %>%
  pr_post("/submit", function(req, res){
    # ...

The “handler” functions that you define in these 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 route methods take 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.

pr() %>%
  pr_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:

pr() %>%
  pr_filter("myFilter", function(req){
    req$filtered <- TRUE
  }) %>%
  pr_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 pr_filter() method, as well.

Registering Hooks on a Router

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 pr_hook method, or you can add multiple hooks at once using the pr_hooks method which takes a name list in which the names are the names of the hooks, and the values are the handlers themselves.

pr() %>%
  pr_hook("preroute", function(req) {
    cat("Routing a request for", req$PATH_INFO, "...\n")
  }) %>%
    preserialize = function(req, value) {
      print("About to serialize this value:")

      # Must return the value since we took one in. Here we're not choosing
      # to mutate it, but we could.
    postserialize = function(res) {
      print("We serialized the value as:")
  )) %>%
  pr_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

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 <- pr()

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

root %>%
  pr_mount("/users", users) %>%
  pr_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 pr_static(). For example

pr() %>%
  pr_static("/assets", "./myfiles") %>%

This will make the files and directories stored in the ./myfiles directory available on your API under the /assets/ path.

Customizing a 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 has already been discussed, but additionally you can modify a router’s behavior using any of the following:

  • pr_set_serializer() - Sets the default serializer of the router.
  • pr_set_error() - Sets the error handler which gets invoked if any filter or endpoint generates an error.
  • pr_set_404() - Sets the handler that gets called if an incoming request can’t be served by any filter, endpoint, or sub-router.