<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
  <channel>
    <title>plumber R package on R Views</title>
    <link>https://rviews.rstudio.com/tags/plumber-r-package/</link>
    <description>Recent content in plumber R package on R Views</description>
    <generator>Hugo -- gohugo.io</generator>
    <language>en-us</language>
    <lastBuildDate>Tue, 13 Aug 2019 00:00:00 +0000</lastBuildDate>
    <atom:link href="https://rviews.rstudio.com/tags/plumber-r-package/" rel="self" type="application/rss+xml" />
    
    
    
    
    <item>
      <title>Plumber Logging</title>
      <link>https://rviews.rstudio.com/2019/08/13/plumber-logging/</link>
      <pubDate>Tue, 13 Aug 2019 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2019/08/13/plumber-logging/</guid>
      <description>
        


&lt;p&gt;The &lt;a href=&#34;https://www.rplumber.io/docs/&#34;&gt;plumber R package&lt;/a&gt; is used to expose R functions as API endpoints. Due to plumber’s incredible flexibility, most major API design decisions are left up to the developer. One important consideration to be made when developing APIs is how to log information about API requests and responses. This information can be used to determine how plumber APIs are performing and how they are being utilized.&lt;/p&gt;
&lt;p&gt;An example of logging API requests in plumber is included in the &lt;a href=&#34;https://www.rplumber.io/docs/routing-and-input.html#filters&#34;&gt;package documentation&lt;/a&gt;. That example uses a filter to log information about incoming requests before a response has been generated. This is certainly a valid approach, but it means that the log cannot contain details about the response since the response hasn’t been created yet. In this post we will look at an alternative approach to logging plumber APIs that uses &lt;a href=&#34;https://www.rplumber.io/docs/programmatic-usage.html#router-hooks&#34;&gt;preroute and postroute hooks&lt;/a&gt; to log information about each API request and its associated response.&lt;/p&gt;
&lt;div id=&#34;logging&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Logging&lt;/h2&gt;
&lt;p&gt;In this example, we will use the &lt;a href=&#34;https://daroczig.github.io/logger/&#34;&gt;logger package&lt;/a&gt; to generate the actual log entries. Using this package isn’t required, but it does provide some convenient functionality that we will take advantage of.&lt;/p&gt;
&lt;p&gt;Since we will be registering hooks for our API, we will need both a &lt;code&gt;plumber.R&lt;/code&gt; file and an &lt;code&gt;entrypoint.R&lt;/code&gt; file. The &lt;code&gt;plumber.R&lt;/code&gt; file contains the following:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# plumber.R
# A simple API to illustrate logging with Plumber

library(plumber)

#* @apiTitle Logging Example

#* @apiDescription Simple example API for implementing logging with Plumber

#* Echo back the input
#* @param msg The message to echo
#* @get /echo
function(msg = &amp;quot;&amp;quot;) {
  list(msg = paste0(&amp;quot;The message is: &amp;#39;&amp;quot;, msg, &amp;quot;&amp;#39;&amp;quot;))
}

#* Plot a histogram
#* @png
#* @get /plot
function() {
  rand &amp;lt;- rnorm(100)
  hist(rand)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we’ve defined two endpoints (&lt;code&gt;/echo&lt;/code&gt; and &lt;code&gt;/plot&lt;/code&gt;), we can use &lt;code&gt;entrypoint.R&lt;/code&gt; to setup logging using preroute and postroute hooks. First, we need to configure the logger package:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# entrypoint.R
library(plumber)

# logging
library(logger)

# Specify how logs are written
log_dir &amp;lt;- &amp;quot;logs&amp;quot;
if (!fs::dir_exists(log_dir)) fs::dir_create(log_dir)
log_appender(appender_tee(tempfile(&amp;quot;plumber_&amp;quot;, log_dir, &amp;quot;.log&amp;quot;)))&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The &lt;code&gt;log_appender()&lt;/code&gt; function is used to specify which appender method is used for logging. Here we use &lt;code&gt;appender_tee()&lt;/code&gt; so that logs will be written to &lt;code&gt;stdout&lt;/code&gt; and to a specific file path. We create a directory called &lt;code&gt;logs/&lt;/code&gt; in the current working directory to store the resulting logs. Every log file is assigned a unique name using &lt;code&gt;tempfile()&lt;/code&gt;. This prevents errors that can occur if concurrent processes try to write to the same file.&lt;/p&gt;
&lt;p&gt;Now, we need to create a helper function that we will use when creating log entries:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;convert_empty &amp;lt;- function(string) {
  if (string == &amp;quot;&amp;quot;) {
    &amp;quot;-&amp;quot;
  } else {
    string
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This function takes an empty string and converts it into a dash (&lt;code&gt;&amp;quot;-&amp;quot;&lt;/code&gt;). We will use this to ensure that empty log values still get recorded so that it is easy to read the log files. We’re now ready to create our plumber router and register the hooks necessary for logging:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;pr &amp;lt;- plumb(&amp;quot;plumber.R&amp;quot;)

pr$registerHooks(
  list(
    preroute = function() {
      # Start timer for log info
      tictoc::tic()
    },
    postroute = function(req, res) {
      end &amp;lt;- tictoc::toc(quiet = TRUE)
      # Log details about the request and the response
      log_info(&amp;#39;{convert_empty(req$REMOTE_ADDR)} &amp;quot;{convert_empty(req$HTTP_USER_AGENT)}&amp;quot; {convert_empty(req$HTTP_HOST)} {convert_empty(req$REQUEST_METHOD)} {convert_empty(req$PATH_INFO)} {convert_empty(res$status)} {round(end$toc - end$tic, digits = getOption(&amp;quot;digits&amp;quot;, 5))}&amp;#39;)
    }
  )
)

pr&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;We use the &lt;code&gt;$registerHooks()&lt;/code&gt; method to register both preroute and postroute hooks. The preroute hook uses the &lt;a href=&#34;http://collectivemedia.github.io/tictoc/&#34;&gt;tictoc package&lt;/a&gt; to start a timer. The postroute hook stops the timer and then writes a log entry using the &lt;code&gt;log_info()&lt;/code&gt; function from the logger package. Each log entry contains the following information:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Log level: This is a distinction made by the logger package, and in this
example the value is always INFO&lt;/li&gt;
&lt;li&gt;Timestamp: The timestamp for when the response was generated and sent back to
the client&lt;/li&gt;
&lt;li&gt;Remote Address: The address of the client making the request&lt;/li&gt;
&lt;li&gt;User Agent: The user agent making the request&lt;/li&gt;
&lt;li&gt;Http Host: The host of the API&lt;/li&gt;
&lt;li&gt;Method: The HTTP method attached to the request&lt;/li&gt;
&lt;li&gt;Path: The specific API endpoint requested&lt;/li&gt;
&lt;li&gt;Status: The HTTP status of the response&lt;/li&gt;
&lt;li&gt;Execution Time: The amount of time from when the request received until the response was generated&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;This log format is loosely inspired by the &lt;a href=&#34;https://en.wikipedia.org/wiki/Common_Log_Format&#34;&gt;NCSA Common log format&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;testing&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Testing&lt;/h2&gt;
&lt;p&gt;Now that our API is all setup, it’s time to test to make sure logging works as expected. First, we need to start the API. The easiest way to do this is to click the Run API button that appears at the top of the &lt;code&gt;plumber.R&lt;/code&gt; file in the RStudio IDE. Once the API is running, you’ll see a message in the console similar to the following:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;Running plumber API at http://127.0.0.1:5762&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now that we know the API is running, we need to make a request. One of the easiest ways to make a request in this case is to open a web browser (like Google Chrome) and type the API address in the address bar followed by &lt;code&gt;/plot&lt;/code&gt;. In this example, I would type &lt;code&gt;http://127.0.0.1:5762/plot&lt;/code&gt; into the address bar of my browser. If all goes well, you should see a plot rendered in the browser. The RStudio console will display the log output:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;INFO [2019-08-09 12:30:23] 127.0.0.1 &amp;quot;Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_6) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/76.0.3809.100 Safari/537.36&amp;quot; localhost:5762 GET /plot 200 0.158&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;A new &lt;code&gt;logs/&lt;/code&gt; directory will have been created in the current working directory and it will contain a file with the log entry. You can generate more log entries by refreshing your browser window.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;analyzing&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Analyzing&lt;/h2&gt;
&lt;p&gt;Let’s say that we refreshed the browser window 1,000 times. The log file generated will contain an entry for each request. We can analyze this log file to find helpful information about the API. For example, we could plot a histogram of execution time:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(ggplot2)

plumber_log &amp;lt;- readr::read_log(&amp;quot;logs/plumber_fe3daed895d.log&amp;quot;,
                               col_names = c(&amp;quot;log_level&amp;quot;,
                                             &amp;quot;timestamp&amp;quot;,
                                             &amp;quot;remote_address&amp;quot;,
                                             &amp;quot;user_agent&amp;quot;,
                                             &amp;quot;http_host&amp;quot;,
                                             &amp;quot;method&amp;quot;,
                                             &amp;quot;path&amp;quot;,
                                             &amp;quot;status&amp;quot;,
                                             &amp;quot;execution_time&amp;quot;))

ggplot(plumber_log, aes(x = execution_time)) +
  geom_histogram() +
  theme_bw() +
  labs(title = &amp;quot;Execution Times&amp;quot;,
       x = &amp;quot;Execution Time&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;&lt;img src=&#34;/post/2019-08-10-plumber-logging/index_files/figure-html/unnamed-chunk-1-1.png&#34; width=&#34;672&#34; /&gt;&lt;/p&gt;
&lt;p&gt;We could even build a &lt;a href=&#34;http://shiny.rstudio.com&#34;&gt;Shiny application&lt;/a&gt; to monitor the &lt;code&gt;logs/&lt;/code&gt; directory and provide real-time visibility into API metrics!&lt;/p&gt;
&lt;p&gt;&lt;img src=&#34;log-monitoring.gif&#34; /&gt;&lt;/p&gt;
&lt;p&gt;The details of this Shiny application go beyond the scope of this post, but the source code is available &lt;a href=&#34;https://github.com/sol-eng/plumber-logging/blob/master/R/shiny/app.R&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;Plumber APIs published to &lt;a href=&#34;https://www.rstudio.com/products/connect/&#34;&gt;RStudio Connect&lt;/a&gt; can use this pattern to log and monitor API requests. Details on this use case can be found in &lt;a href=&#34;https://github.com/sol-eng/plumber-logging#deployment&#34;&gt;this repository&lt;/a&gt;&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;conclusion&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Plumber is an incredibly flexible package for exposing R functions as API endpoints. Logging information about API requests and responses provides visibility into API usage and performance. These log files can be manually inspected or used in connection with other tools (like Shiny) to provide real-time metrics around API use. The code used in this example along with additional information is available in &lt;a href=&#34;https://github.com/sol-eng/plumber-logging&#34;&gt;this GitHub repository&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;If you are interested in learning more about using plumber, logging, Shiny, and RStudio Connect, please visit &lt;a href=&#34;https://community.rstudio.com/&#34;&gt;community.rstudio.com&lt;/a&gt; and let us know!&lt;/p&gt;
&lt;p&gt;&lt;em&gt;James Blair is a solutions engineer at RStudio who focuses on tools,
technologies, and best practices for using R in the enterprise.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2019/08/13/plumber-logging/&#39;;&lt;/script&gt;
      </description>
    </item>
    
    <item>
      <title>Slack and Plumber, Part Two</title>
      <link>https://rviews.rstudio.com/2018/11/27/slack-and-plumber-part-two/</link>
      <pubDate>Tue, 27 Nov 2018 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2018/11/27/slack-and-plumber-part-two/</guid>
      <description>
        


&lt;p&gt;This is the final entry in a three-part series about the &lt;a href=&#34;https://www.rplumber.io/&#34;&gt;&lt;code&gt;plumber&lt;/code&gt;&lt;/a&gt; package. &lt;a href=&#34;https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/&#34;&gt;The first post&lt;/a&gt; introduces &lt;code&gt;plumber&lt;/code&gt; as an R package for building REST API endpoints in R. &lt;a href=&#34;https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/&#34;&gt;The second post&lt;/a&gt; builds a working example of a &lt;code&gt;plumber&lt;/code&gt; API that powers a &lt;a href=&#34;https://api.slack.com/slash-commands&#34;&gt;Slack slash command&lt;/a&gt;. In this final entry, we will secure the API created in the previous post so that it only responds to authenticated requests, and deploy it using &lt;a href=&#34;https://www.rstudio.com/products/connect/&#34;&gt;RStudio Connect&lt;/a&gt;.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-11-20-blair-plumber-slack-part-two-files/plumber-slack-demo.gif&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;As a reminder, this API is built on top of simulated customer call data. The slash command we create will allow users to view a customer status report within Slack. This status report contains customer name, total calls, date of birth, and a plot of call history for the past 20 weeks. The simulated data, along with the script used to create it, can be found in the &lt;a href=&#34;https://github.com/sol-eng/plumber-slack&#34;&gt;GitHub repository&lt;/a&gt; for this example.&lt;/p&gt;
&lt;div id=&#34;setup&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;Successfully following this example assumes you have created a &lt;a href=&#34;https://slack.com&#34;&gt;Slack&lt;/a&gt; account and you have &lt;a href=&#34;https://api.slack.com/slack-apps&#34;&gt;followed the instructions for creating an app&lt;/a&gt;. The Plumber API as it currently exists is described in detail in the &lt;a href=&#34;https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/&#34;&gt;previous post&lt;/a&gt;.&lt;/p&gt;
&lt;p&gt;This API can be run through the UI as previously described, or by running &lt;code&gt;plumber::plumb(&amp;quot;plumber.R&amp;quot;)$run(port = 5762)&lt;/code&gt; from the directory containing the API defined in &lt;code&gt;plumber.R&lt;/code&gt;. As it stands now, this API could be deployed and used by Slack. However, it’s important to remember that we have no control over the request that Slack makes to the API. Because of this, we can’t rely on RStudio Connect’s &lt;a href=&#34;http://docs.rstudio.com/connect/admin/content-management.html#api-keys&#34;&gt;built-in API authentication mechanism&lt;/a&gt; to secure the API because there is no way to submit a key with the request. Our options are either to expose the API with no security, meaning anyone can access the endpoints we’ve defined, or to find some other mechanism for securing the API so that it only responds to authorized requests.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;api-security-patterns&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;API Security Patterns&lt;/h2&gt;
&lt;p&gt;The &lt;a href=&#34;https://www.rplumber.io/docs/security.html&#34;&gt;&lt;code&gt;plumber&lt;/code&gt; documentation&lt;/a&gt; provides a good introduction to API security for the R user:&lt;/p&gt;
&lt;blockquote&gt;
&lt;p&gt;The majority of R programmers have not been trained to give much attention to the security of the code that they write. This is for good reason since running R code on your own machine with no external input gives little opportunity for attackers to leverage your R code to do anything malicious. However, as soon as you expose an API on a network, your concerns and thought process must adapt accordingly.&lt;/p&gt;
&lt;/blockquote&gt;
&lt;p&gt;API security can be challenging to address. As it stands today, it is the developer’s responsibility to provide proper security on API endpoints, though in the future, there may be additional security features added to &lt;code&gt;plumber&lt;/code&gt; or available via other R packages.&lt;/p&gt;
&lt;p&gt;As mentioned in the &lt;a href=&#34;https://www.rplumber.io/docs/security.html&#34;&gt;&lt;code&gt;plumber&lt;/code&gt; documentation&lt;/a&gt;, there are a number of things to consider when designing API security. For example, if the API is deployed on an internal network, securing the API may not be as important as it would be if the API was publicly exposed on the internet. When an API needs to be secured, there are several potential attack vectors that need to be handled. In this specific example, we are exposing a public endpoint that provides access to sensitive customer data. If we are unable to authenticate incoming requests, then we risk exposing sensitive data. To prevent this data from falling into the wrong hands, we will focus on verifying incoming requests so that the API only responds to requests made from Slack.&lt;/p&gt;
&lt;p&gt;There are several different methods for authenticating requests made to API endpoints. One common method is the use of API keys, which are cryptographically secure values sent with the request to verify the identity of the client. However, in this case, we have no control over the request Slack sends, so we cannot include such a key in the request. Thankfully, Slack has provided an alternative authentication method using &lt;a href=&#34;https://api.slack.com/docs/verifying-requests-from-slack&#34;&gt;signed secrets&lt;/a&gt;. Full details can be read in the Slack documentation, but in essence, each Slack application is assigned a unique secret value that, when used in connection with other request details, can be used to verify that an incoming request is indeed coming from Slack and not an unknown third party.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;securing-the-api&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Securing the API&lt;/h2&gt;
&lt;p&gt;In order to secure our API so that only requests from Slack are honored, we first need to obtain the signing secret for our application. This value can be found in the Basic Information section of the Slack application settings. Now, it is important to remember that this is called a signing secret for a reason: it should not be shared with anyone. To avoid exposing this secret, we can save it as an environment variable. We add this in a current R session by using &lt;code&gt;Sys.setenv(SLACK_SIGNING_SECRET = &amp;lt;our signing secret&amp;gt;)&lt;/code&gt;, or we can add it to our &lt;a href=&#34;https://csgillespie.github.io/efficientR/set-up.html#renviron&#34;&gt;&lt;code&gt;.Renviron&lt;/code&gt;&lt;/a&gt; file so that it is set for every R session. Once this is done, we can access this value in R using &lt;code&gt;Sys.getenv(&amp;quot;SLACK_SIGNING_SECRET&amp;quot;)&lt;/code&gt;. Now we are ready to create a function to verify if incoming requests are from Slack.&lt;/p&gt;
&lt;p&gt;Slack provides the following three-step process for verifying requests:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Your app receives a request from Slack&lt;/li&gt;
&lt;li&gt;Your app computes a signature based on the request&lt;/li&gt;
&lt;li&gt;You make sure the computed signature matches the signature on the request&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In order to verify all incoming requests, we can define an additional filter for our API that follows the above recipe.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#* Verify incoming requests
#* @filter verify
function(req, res) {
  # Forward requests coming to swagger endpoints
  if (grepl(&amp;quot;swagger&amp;quot;, tolower(req$PATH_INFO))) return(forward())

  # Check for X_SLACK_REQUEST_TIMESTAMP header
  if (is.null(req$HTTP_X_SLACK_REQUEST_TIMESTAMP)) {
    res$status &amp;lt;- 401
  }

  # Build base string
  base_string &amp;lt;- paste(
    &amp;quot;v0&amp;quot;,
    req$HTTP_X_SLACK_REQUEST_TIMESTAMP,
    req$postBody,
    sep = &amp;quot;:&amp;quot;
  )

  # Slack Signing secret is available as environment variable
  # SLACK_SIGNING_SECRET
  computed_request_signature &amp;lt;- paste0(
    &amp;quot;v0=&amp;quot;,
    openssl::sha256(base_string, Sys.getenv(&amp;quot;SLACK_SIGNING_SECRET&amp;quot;))
  )

  # If the computed request signature doesn&amp;#39;t match the signature provided in the
  # request, set status of response to 401
  if (!identical(req$HTTP_X_SLACK_SIGNATURE, computed_request_signature)) {
    res$status &amp;lt;- 401
  } else {
    res$status &amp;lt;- 200
  }

  if (res$status == 401) {
    list(
      text = &amp;quot;Error: Invalid request&amp;quot;
    )
  } else {
    forward()
  }
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are a lot of moving pieces to this filter, but essentially we are following the process outlined by Slack for verifying requests. We also allow Swagger endpoints to be served without verification so that the Swagger UI can still be generated for our API.&lt;/p&gt;
&lt;p&gt;Once this filter is in place, all incoming requests will be verified. However, this will create issues with our &lt;code&gt;/plot/history/&lt;/code&gt; endpoint since it is called using a standard GET request without any Slack authentication. To ensure that this endpoint is able to be utilized as we want, we’ll make some small updates to the endpoint and add &lt;code&gt;#* @preempt verify&lt;/code&gt; to the &lt;code&gt;plumber&lt;/code&gt; comments before the function. This prevents the &lt;code&gt;verify&lt;/code&gt; filter from applying to this endpoint.&lt;/p&gt;
&lt;p&gt;Now, this prevents the Slack authentication process from applying to our plot endpoint. However, this endpoint, if left unsecured, provides unfiltered access to sensitive customer data. We need an effective way to secure this endpoint so that it only responds to requests generated from Slack.&lt;/p&gt;
&lt;p&gt;Since the only thing we control in the request to this endpoint is the URL, we can update our endpoint so that an encrypted parameter is passed as part of the URL. This parameter is a combination of the current datetime and the customer ID that is then encrypted using our Slack signing secret. We can use the &lt;code&gt;encrypt_string()&lt;/code&gt; function from the &lt;a href=&#34;https://talegari.github.io/safer/&#34;&gt;&lt;code&gt;safer&lt;/code&gt;&lt;/a&gt; package to securely encrypt this string. The following example illustrates this process.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;current_time &amp;lt;- Sys.time()
customer_id &amp;lt;- 89
parameter_string &amp;lt;- paste(current_time, customer_id, sep = &amp;quot;;&amp;quot;)
safer::encrypt_string(parameter_string, Sys.getenv(&amp;quot;SLACK_SIGNING_SECRET&amp;quot;))&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## [1] &amp;quot;m7NfMZfpY1n5EuivjuiFQsyKopT68HiX+NIgk5S+VBlDHrVqzRM=&amp;quot;&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once we have created this encrypted value, we pass it to the URL of our plot endpoint. Then, within the plot endpoint, we decrypt the string, extract the customer ID, and check to see if the current time is within five seconds of the time encoded in the string. If more than five seconds have passed, we consider the request to be unauthorized. To help with this process, we define two helper functions:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;encrypt_string &amp;lt;- function(string) {
  urltools::url_encode(safer::encrypt_string(paste(Sys.time(), string, sep = &amp;quot;;&amp;quot;),
                                             key = Sys.getenv(&amp;quot;SLACK_SIGNING_SECRET&amp;quot;)))
}

plot_auth &amp;lt;- function(endpoint, time_limit = 5) {
  # Save current time to compare against endpoint time value
  current_time &amp;lt;- Sys.time()

  # Try to decrypt endpoint and extract user id
  tryCatch({
    # Decrypt endpoint using SLACK_SIGNING_SECRET
    decrypted_endpoint &amp;lt;- safer::decrypt_string(endpoint,
                                                key = Sys.getenv(&amp;quot;SLACK_SIGNING_SECRET&amp;quot;))
    # Split endpoint on ;
    endpoint_split &amp;lt;- unlist(strsplit(decrypted_endpoint, split = &amp;quot;;&amp;quot;))
    # Convert time
    endpoint_time &amp;lt;- as.POSIXct(endpoint_split[1])
    # Calculate time difference
    time_diff &amp;lt;- difftime(current_time, endpoint_time, units = &amp;quot;secs&amp;quot;)

    # If more than 5 seconds have passed since the request was generated, then
    # error
    if (time_diff &amp;gt; time_limit) {
      &amp;quot;Unauthorized&amp;quot;
    } else {
      endpoint_split[2]
    }
  },
  error = function(e) &amp;quot;Unauthorized&amp;quot;
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once these helper functions are in place, we can update our &lt;code&gt;/plot/history&lt;/code&gt; endpoint as follows:&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#* Plot customer weekly calls
#* @png
#* @param cust_secret encrypted value calculated in /status endpoint
#* @response 400 No customer with the given ID was found.
#* @preempt verify
#* @get /plot/history
function(res, cust_secret) {
  # Authenticate that request came from /status
  cust_id &amp;lt;- plot_auth(cust_secret)

  # Return unauthorized error if cust_id is &amp;quot;Unauthorized&amp;quot;
  if (cust_id == &amp;quot;Unauthorized&amp;quot;) {
    res$status &amp;lt;- 401
    stop(&amp;quot;Unauthorized request&amp;quot;)
  } else if (!cust_id %in% sim_data$id) {
    res$status &amp;lt;- 400
    stop(&amp;quot;Customer id&amp;quot; , cust_id, &amp;quot; not found.&amp;quot;)
  }

  # Filter data to customer id provided
  plot_data &amp;lt;- dplyr::filter(sim_data, id == cust_id)

  # Customer name (id)
  customer_name &amp;lt;- paste0(unique(plot_data$name), &amp;quot; (&amp;quot;, unique(plot_data$id), &amp;quot;)&amp;quot;)

  # Create plot
  history_plot &amp;lt;- plot_data %&amp;gt;%
    ggplot(aes(x = time, y = calls, col = calls)) +
    ggalt::geom_lollipop(show.legend = FALSE) +
    theme_light() +
    labs(
      title = paste(&amp;quot;Weekly calls for&amp;quot;, customer_name),
      x = &amp;quot;Week&amp;quot;,
      y = &amp;quot;Calls&amp;quot;
    )

  # print() is necessary to render plot properly
  print(history_plot)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Now we need to make one small change to our &lt;code&gt;/status&lt;/code&gt; endpoint so that we properly build the appropriate URL for our image. We construct the list response to the &lt;code&gt;/status&lt;/code&gt; endpoint as follows, where &lt;code&gt;image_url&lt;/code&gt; has been updated.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;    attachments = list(
      list(
        color = customer_status,
        title = paste0(&amp;quot;Status update for &amp;quot;, customer_name, &amp;quot; (&amp;quot;, customer_id, &amp;quot;)&amp;quot;),
        fallback = paste0(&amp;quot;Status update for &amp;quot;, customer_name, &amp;quot; (&amp;quot;, customer_id, &amp;quot;)&amp;quot;),
        # History plot

        image_url = paste0(base_url,
                           &amp;quot;/plot/history?cust_secret=&amp;quot;,
                           encrypt_string(customer_id)),
        # Fields provide a way of communicating semi-tabular data in Slack
        fields = list(
          list(
            title = &amp;quot;Total Calls&amp;quot;,
            value = sum(customer_data$calls),
            short = TRUE
          ),
          list(
            title = &amp;quot;DoB&amp;quot;,
            value = unique(customer_data$dob),
            short = TRUE
          )
        )
      )
    )&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Just like that, we have a secure API!&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;all-together-now&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;All Together Now&lt;/h2&gt;
&lt;p&gt;Now, given the authorization pieces we have implemented, it is a bit more difficult to test our API since our endpoints will only respond to authorized requests. However, we can use the free version of &lt;a href=&#34;https://www.getpostman.com&#34;&gt;Postman&lt;/a&gt; to test our API. An in-depth look at the capabilities of Postman is beyond the scope of this post, so hopefully a gif will suffice. Further details about using Postman in this context can be found in the &lt;a href=&#34;https://github.com/sol-eng/plumber-slack#running-locally&#34;&gt;GitHub repository&lt;/a&gt; for this example.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-11-20-blair-plumber-slack-part-two-files/postman-demo.gif&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;It appears that everything is working as expected! Our endpoints fail when the authorization criteria are not met, and otherwise they succeed. Notice that the plot endpoint works when initially called, but when a subsequent call is made it fails since more than five seconds have passed since the &lt;code&gt;/status&lt;/code&gt; endpoint was invoked.&lt;/p&gt;
&lt;p&gt;Now, the final step in this process is publishing this API so that Slack can properly interact with it. The easiest way to do this is to publish the API to &lt;a href=&#34;http://docs.rstudio.com/connect/user/publishing.html#publishing-apis&#34;&gt;RStudio Connect&lt;/a&gt;. Once published, Slack can be updated to point the Slash command to our nice, newly secured API.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;conclusion&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;This brings us to the conclusion of this series. We’ve discovered the power of &lt;code&gt;plumber&lt;/code&gt; in exposing R to downstream consumers via RESTful API endpoints. We built a Slack app powered entirely by R and &lt;code&gt;plumber&lt;/code&gt;, and now we have secured the underlying API so that it only responds to authorized requests. As we have seen, &lt;code&gt;plumber&lt;/code&gt; provides a powerful and flexible framework for exposing R functions as APIs. These APIs can be safely secured so that only authorized requests are permitted.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;James Blair is a solutions engineer at RStudio who focuses on tools, technologies, and best practices for using R in the enterprise.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2018/11/27/slack-and-plumber-part-two/&#39;;&lt;/script&gt;
      </description>
    </item>
    
    <item>
      <title>Slack and Plumber, Part One</title>
      <link>https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/</link>
      <pubDate>Thu, 30 Aug 2018 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/</guid>
      <description>
        


&lt;p&gt;In &lt;a href=&#34;https://rviews.rstudio.com/2018/07/23/rest-apis-and-plumber/&#34;&gt;the previous post&lt;/a&gt;, we introduced &lt;a href=&#34;https://www.rplumber.io&#34;&gt;&lt;code&gt;plumber&lt;/code&gt;&lt;/a&gt; as a way to expose R processes and programs to external systems via REST API endpoints. In this post, we’ll go further by building out an API that powers a &lt;a href=&#34;https://api.slack.com/slash-commands&#34;&gt;Slack slash command&lt;/a&gt;, all from within R using &lt;code&gt;plumber&lt;/code&gt;. A subsequent post will outline deploying and securing the API.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-08-15-blair-plumber-slack-files/slash-command-preview.png&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;We will create an API built on top of simulated customer call data that powers a slash command. This command will allows users to view a customer status report within Slack. As shown, this status report contains customer name, total calls, date of birth, and a plot of call history for the past 20 weeks. The simulated data, along with the script used to create it, can be found in the &lt;a href=&#34;https://github.com/sol-eng/plumber-slack&#34;&gt;GitHub repository&lt;/a&gt; for this example.&lt;/p&gt;
&lt;div id=&#34;setup&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Setup&lt;/h2&gt;
&lt;p&gt;&lt;a href=&#34;https://slack.com&#34;&gt;Slack&lt;/a&gt; is a commonly used communication tool that’s highly customizable through various integrations. It’s even possible to build your own integrations, which is what we’ll be doing here. In order to build a Slack app, you need to have a Slack account and follow &lt;a href=&#34;https://api.slack.com/slack-apps&#34;&gt;the instructions for creating an app&lt;/a&gt;. In this example, we will build an app that includes a slash command that users can access by typing &lt;code&gt;/&amp;lt;command-name&amp;gt;&lt;/code&gt; into a Slack message.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;the-slack-request&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;The Slack request&lt;/h2&gt;
&lt;p&gt;In this scenario, we’re building an API that will interact with a known request. This means that we need to understand the nature of the incoming request so that we can appropriately handle it within the API. Slack provides &lt;a href=&#34;https://api.slack.com/slash-commands#app_command_handling&#34;&gt;some documentation&lt;/a&gt; about the request that is sent when a slash command is invoked. In short, an HTTP POST request is made that contains a URL-encoded data payload. An example data payload looks like:&lt;/p&gt;
&lt;pre&gt;&lt;code&gt;token=gIkuvaNzQIHg97ATvDxqgjtO
&amp;amp;team_id=T0001
&amp;amp;team_domain=example
&amp;amp;enterprise_id=E0001
&amp;amp;enterprise_name=Globular%20Construct%20Inc
&amp;amp;channel_id=C2147483705
&amp;amp;channel_name=test
&amp;amp;user_id=U2147483697
&amp;amp;user_name=Steve
&amp;amp;command=/weather
&amp;amp;text=94070
&amp;amp;response_url=https://hooks.slack.com/commands/1234/5678
&amp;amp;trigger_id=13345224609.738474920.8088930838d88f008e0&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There’s a lot of detail included in the Slack request, and the &lt;a href=&#34;https://api.slack.com/slash-commands#app_command_handling&#34;&gt;Slack documentation&lt;/a&gt; provides details about each field. We’re mainly interested in the &lt;code&gt;text&lt;/code&gt; field, which contains the text entered into Slack after the slash command. In the above example, the user entered &lt;code&gt;/weather 94070&lt;/code&gt; into Slack, so the request indicates that the command was &lt;code&gt;/weather&lt;/code&gt; and the text was &lt;code&gt;94070&lt;/code&gt;.&lt;/p&gt;
&lt;p&gt;Note that this approach is different from APIs that are not being built around a known request or specification. In such instances, we are free to expose endpoints and return data in whatever method seems most beneficial to downstream consumers. In such a scenario, we would provide downstream API consumers with an understanding of how the API handles requests and what types of responses are generated so that they can appropriately interact with the API. But in this example, the design of our API is, in part, dictated by the specifications Slack provides.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;building-the-api&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Building the API&lt;/h2&gt;
&lt;p&gt;Now that we have an understanding of what is included in the incoming request, we can begin to build out the API using &lt;code&gt;plumber&lt;/code&gt;. First, we need to set up the global environment for the API by loading necessary packages and global objects, including the simulated data this API is built on. In reality, this data would likely come from an external database accessed via an ODBC connection.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;# Packages ----
library(plumber)
library(magrittr)
library(ggplot2)

# Data ----
# Load sample customer data
sim_data &amp;lt;- readr::read_rds(&amp;quot;data/sim-data.rds&amp;quot;)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;The following diagram outlines what we want to build.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-08-15-blair-plumber-slack-files/plumber-slack-architecture.png&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;In essence, an incoming request will pass through two &lt;a href=&#34;https://www.rplumber.io/docs/routing-and-input.html#filters&#34;&gt;filters&lt;/a&gt; before reaching an endpoint. This first filter is responsible for routing incoming requests to the correct endpoint. This is done so that a single slash command can serve multiple endpoints without the need to create separate commands for each service. The second filter simply logs details about the request for future review.&lt;/p&gt;
&lt;div id=&#34;filters&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Filters&lt;/h3&gt;
&lt;p&gt;The first filter is responsible for parsing the incoming request and ensuring it is assigned to the appropriate endpoint. This is done because when a slash command is created in Slack, there is only one endpoint defined for requests made from the command. This filter enables several endpoints to be utilized by the same slash command by parsing the incoming &lt;code&gt;text&lt;/code&gt; of the command and treating the first value of that command as the endpoint to which the request should be routed.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#* Parse the incoming request and route it to the appropriate endpoint
#* @filter route-endpoint
function(req, text = &amp;quot;&amp;quot;) {
  # Identify endpoint
  split_text &amp;lt;- urltools::url_decode(text) %&amp;gt;%
    strsplit(&amp;quot; &amp;quot;) %&amp;gt;%
    unlist()
  
  if (length(split_text) &amp;gt;= 1) {
    endpoint &amp;lt;- split_text[[1]]
    
    # Modify request with updated endpoint
    req$PATH_INFO &amp;lt;- paste0(&amp;quot;/&amp;quot;, endpoint)
    
    # Modify request with remaining commands from text
    req$ARGS &amp;lt;- split_text[-1] %&amp;gt;% 
      paste0(collapse = &amp;quot; &amp;quot;)
  }
  
  # Forward request 
  forward()
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;This filter requires an understanding of the &lt;a href=&#34;https://www.rplumber.io/docs/routing-and-input.html#the-request-object&#34;&gt;&lt;code&gt;req&lt;/code&gt; object&lt;/a&gt;. It’s important to note that a few things happen in this filter. First, we parse the &lt;code&gt;text&lt;/code&gt; argument and use the first part of &lt;code&gt;text&lt;/code&gt; as the &lt;code&gt;req$PATH_INFO&lt;/code&gt;, which tells &lt;code&gt;plumber&lt;/code&gt; where to route the request. Second, we take anything remaining from &lt;code&gt;text&lt;/code&gt; and attach it to the request in &lt;code&gt;req$ARGS&lt;/code&gt;. This means that any downstream filters or endpoints will have access to &lt;code&gt;req$ARGS&lt;/code&gt;. The second filter is taken straight from the &lt;a href=&#34;https://www.rplumber.io/docs/routing-and-input.html#forward-to-another-handler&#34;&gt;&lt;code&gt;plumber&lt;/code&gt; documentation&lt;/a&gt; and simply logs details about the incoming request.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#* Log information about the incoming request
#* @filter logger
function(req){
  cat(as.character(Sys.time()), &amp;quot;-&amp;quot;, 
      req$REQUEST_METHOD, req$PATH_INFO, &amp;quot;-&amp;quot;, 
      req$HTTP_USER_AGENT, &amp;quot;@&amp;quot;, req$REMOTE_ADDR, &amp;quot;\n&amp;quot;)
  
  # Forward request
  forward()
}&lt;/code&gt;&lt;/pre&gt;
&lt;/div&gt;
&lt;div id=&#34;endpoints&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;Endpoints&lt;/h3&gt;
&lt;p&gt;There are a few endpoints we need to define. First, we need to define an endpoint that provides a response Slack can understand and interpret into a message. In this case, we’re going to return a &lt;a href=&#34;https://www.json.org&#34;&gt;JSON object&lt;/a&gt; that Slack interprets into a message with attachments. Slack provides &lt;a href=&#34;https://api.slack.com/docs/message-attachments&#34;&gt;detailed documentation&lt;/a&gt; on what fields it accepts in a response. Also note that Slack expects unboxed JSON, while the &lt;a href=&#34;https://www.rplumber.io/docs/rendering-and-output.html#boxed-vs-unboxed-json&#34;&gt;&lt;code&gt;plumber&lt;/code&gt; default&lt;/a&gt; is to return boxed JSON. In order to ensure that Slack understands the response, we set the serializer for this response to be &lt;code&gt;unboxedJSON&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#* Return a message containing status details about the customer
#* @serializer unboxedJSON
#* @post /status
function(req, res) {
  # Check req$ARGS and match to customer - if no customer match is found, return
  # an error
  
  customer_ids &amp;lt;- unique(sim_data$id)
  customer_names &amp;lt;- unique(sim_data$name)
  
  if (!as.numeric(req$ARGS) %in% customer_ids &amp;amp; !req$ARGS %in% customer_names) {
    res$status &amp;lt;- 400
    return(
      list(
        response_type = &amp;quot;ephemeral&amp;quot;,
        text = paste(&amp;quot;Error: No customer found matching&amp;quot;, req$ARGS)
      )
    )
  }
  
  # Filter data to customer data based on provided id / name
  if (as.numeric(req$ARGS) %in% customer_ids) {
    customer_id &amp;lt;- as.numeric(req$ARGS)
    customer_data &amp;lt;- dplyr::filter(sim_data, id == customer_id)
    customer_name &amp;lt;- unique(customer_data$name)
  } else {
    customer_name &amp;lt;- req$ARGS
    customer_data &amp;lt;- dplyr::filter(sim_data, name == customer_name)
    customer_id &amp;lt;- unique(customer_data$id)
  }
  
  # Simple heuristics for customer status
  total_customer_calls &amp;lt;- sum(customer_data$calls)
  
  customer_status &amp;lt;- dplyr::case_when(total_customer_calls &amp;gt; 250 ~ &amp;quot;danger&amp;quot;,
                                      total_customer_calls &amp;gt; 130 ~ &amp;quot;warning&amp;quot;,
                                      TRUE ~ &amp;quot;good&amp;quot;)
  
  # Build response
  list(
    # response type - ephemeral indicates the response will only be seen by the
    # user who invoked the slash command as opposed to the entire channel
    response_type = &amp;quot;ephemeral&amp;quot;,
    # attachments is expected to be an array, hence the list within a list
    attachments = list(
      list(
        color = customer_status,
        title = paste0(&amp;quot;Status update for &amp;quot;, customer_name, &amp;quot; (&amp;quot;, customer_id, &amp;quot;)&amp;quot;),
        fallback = paste0(&amp;quot;Status update for &amp;quot;, customer_name, &amp;quot; (&amp;quot;, customer_id, &amp;quot;)&amp;quot;),
        # History plot
        image_url = paste0(&amp;quot;localhost:5762/plot/history/&amp;quot;, customer_id),
        # Fields provide a way of communicating semi-tabular data in Slack
        fields = list(
          list(
            title = &amp;quot;Total Calls&amp;quot;,
            value = sum(customer_data$calls),
            short = TRUE
          ),
          list(
            title = &amp;quot;DoB&amp;quot;,
            value = unique(customer_data$dob),
            short = TRUE
          )
        )
      )
    )
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;There are three main things that happen in this endpoint. First, we check to ensure that the provided customer name or ID appear in the dataset. Next, we create a subset of the data for only the identified customer and use a simple heuristic to determine the customer’s status. Finally, we put a list together that will be serialized into JSON in response to requests made to this endpoint. This list conforms to the standards outlined by Slack.&lt;/p&gt;
&lt;p&gt;The second endpoint is used to provide the history plot that is referenced in the first endpoint. When an &lt;code&gt;image_url&lt;/code&gt; is provided in a Slack attachment, Slack uses a GET request to fetch the image from the URL. So, this endpoint responds to incoming GET requests with an image.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;#* Plot customer weekly calls
#* @png
#* @param cust_id ID of the customer
#* @get /plot/history/&amp;lt;cust_id:int&amp;gt;
function(cust_id, res) {
  # Throw error if cust_id doesn&amp;#39;t exist in data
  if (!cust_id %in% sim_data$id) {
    res$status &amp;lt;- 400
    stop(&amp;quot;Customer id&amp;quot; , cust_id, &amp;quot; not found.&amp;quot;)
  }
  
  # Filter data to customer id provided
  plot_data &amp;lt;- dplyr::filter(sim_data, id == cust_id)
  
  # Customer name (id)
  customer_name &amp;lt;- paste0(unique(plot_data$name), &amp;quot; (&amp;quot;, unique(plot_data$id), &amp;quot;)&amp;quot;)
  
  # Create plot
  history_plot &amp;lt;- plot_data %&amp;gt;%
    ggplot(aes(x = time, y = calls, col = calls)) +
    ggalt::geom_lollipop(show.legend = FALSE) +
    theme_light() +
    labs(
      title = paste(&amp;quot;Weekly calls for&amp;quot;, customer_name),
      x = &amp;quot;Week&amp;quot;,
      y = &amp;quot;Calls&amp;quot;
    )
  
  # print() is necessary to render plot properly
  print(history_plot)
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once these pieces are together, you can run the API either through the UI as described in the previous post, or by running &lt;code&gt;plumber::plumb(&amp;quot;plumber.R&amp;quot;)$run(port = 5762)&lt;/code&gt; from the directory containing the API.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;testing-the-api&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Testing the API&lt;/h2&gt;
&lt;p&gt;Once the API is up and running, we can test it to make sure it’s behaving as we expect. Since the main point of contact is making a POST request to the &lt;code&gt;/status&lt;/code&gt; endpoint, it’s easiest to interact with the API through &lt;code&gt;curl&lt;/code&gt;.&lt;/p&gt;
&lt;pre class=&#34;bash&#34;&gt;&lt;code&gt;$ curl -X POST --data &amp;#39;{&amp;quot;text&amp;quot;:&amp;quot;status 1&amp;quot;}&amp;#39; localhost:5762 | jq &amp;#39;.&amp;#39;
{
  &amp;quot;response_type&amp;quot;: &amp;quot;ephemeral&amp;quot;,
  &amp;quot;attachments&amp;quot;: [
    {
      &amp;quot;color&amp;quot;: &amp;quot;good&amp;quot;,
      &amp;quot;title&amp;quot;: &amp;quot;Status update for Rahul Wilderman IV (001)&amp;quot;,
      &amp;quot;fallback&amp;quot;: &amp;quot;Status update for Rahul Wilderman IV (001)&amp;quot;,
      &amp;quot;image_url&amp;quot;: &amp;quot;localhost:5762/plot/history/001&amp;quot;,
      &amp;quot;fields&amp;quot;: [
        {
          &amp;quot;title&amp;quot;: &amp;quot;Total Calls&amp;quot;,
          &amp;quot;value&amp;quot;: 27,
          &amp;quot;short&amp;quot;: true
        },
        {
          &amp;quot;title&amp;quot;: &amp;quot;DoB&amp;quot;,
          &amp;quot;value&amp;quot;: &amp;quot;2004-04-01&amp;quot;,
          &amp;quot;short&amp;quot;: true
        }
      ]
    }
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Success! Our API successfully routed our request to the appropriate endpoint and returned a valid JSON response. As a final check, we can visit the &lt;code&gt;image_url&lt;/code&gt; in our browser to see if the plot is properly rendered.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-08-15-blair-plumber-slack-files/plot-screen-shot.png&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;Everything appears to be running as expected!&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;conclusion&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;In this post, we used &lt;code&gt;plumber&lt;/code&gt; to create an API that can properly interact with the Slack slash command interface. In the next post, we will explore API security and deployment. Continuing with this example, we will secure our API using &lt;a href=&#34;https://api.slack.com/docs/verifying-requests-from-slack&#34;&gt;Slack’s guidelines&lt;/a&gt;, deploy the API, and finally connect Slack so that we can use our new slash command. At the conclusion of the next post, we will have a fully functioning Slack slash command, all built using R.&lt;/p&gt;
&lt;p&gt;&lt;em&gt;James Blair is a solutions engineer at RStudio who focusses on tools, technologies, and best practices for using R in the enterprise.&lt;/em&gt;&lt;/p&gt;
&lt;/div&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2018/08/30/slack-and-plumber-part-one/&#39;;&lt;/script&gt;
      </description>
    </item>
    
    <item>
      <title>REST APIs and Plumber</title>
      <link>https://rviews.rstudio.com/2018/07/23/rest-apis-and-plumber/</link>
      <pubDate>Mon, 23 Jul 2018 00:00:00 +0000</pubDate>
      
      <guid>https://rviews.rstudio.com/2018/07/23/rest-apis-and-plumber/</guid>
      <description>
        


&lt;p&gt;Moving R resources from development to production can be a challenge, especially when the resource isn’t something like a &lt;a href=&#34;http://shiny.rstudio.com&#34;&gt;&lt;code&gt;shiny&lt;/code&gt; application&lt;/a&gt; or &lt;a href=&#34;https://rmarkdown.rstudio.com&#34;&gt;&lt;code&gt;rmarkdown&lt;/code&gt; document&lt;/a&gt; that can be easily published and consumed. Consider, as an example, a customer success model created in R. This model is responsible for taking customer data and returning a predicted outcome, like the likelihood the customer will churn. Once this model is developed and validated, there needs to be some way for the model output to be leveraged by other systems and individuals within the company.&lt;/p&gt;
&lt;p&gt;Traditionally, moving this model into production has involved one of two approaches: either running customer data through the model on a batch basis and caching the results in a database, or handing the model definition off to a development team to translate the work done in R into another language, such as Java or Scala. Both approaches have significant downsides. Batch processing works, but it misses real-time updates. For example, if the batch job runs every night and a customer calls in the next morning and has a heated conversation with support, the model output will have no record of that exchange when the customer calls the customer loyalty department later the same day to cancel their service. In essence, model output is served on a lag, which can sometimes lead to critical information loss. However, the other option requires a large investment of time and resources to convert an existing model into another language just for the purpose of exposing that model as a real-time service. Neither of these approaches is ideal; to solve this problem, the optimal solution is to expose the existing R model as a service that can be easily accessed by other parts of the organization.&lt;/p&gt;
&lt;p&gt;&lt;a href=&#34;https://www.rplumber.io&#34;&gt;&lt;code&gt;plumber&lt;/code&gt;&lt;/a&gt; is an R package that allows existing R code to be exposed as a web service through special decorator comments. With minimal overhead, R programmers and analysts can use &lt;code&gt;plumber&lt;/code&gt; to create REST APIs that expose their work to any number of internal and external systems. This solution provides real-time access to processes and services created entirely in R, and can effectively eliminate the need to perform batch operations or technical hand-offs in order to move R code into production.&lt;/p&gt;
&lt;p&gt;This post will focus on a brief introduction to RESTful APIs, then an introduction to the &lt;code&gt;plumber&lt;/code&gt; package and how it can be used to expose R services as API endpoints. In subsequent posts, we’ll build a functioning web API using &lt;code&gt;plumber&lt;/code&gt; that integrates with &lt;a href=&#34;https://slack.com&#34;&gt;Slack&lt;/a&gt; and provides real-time customer status reports.&lt;/p&gt;
&lt;div id=&#34;web-apis&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Web APIs&lt;/h2&gt;
&lt;p&gt;For some, &lt;a href=&#34;https://en.wikipedia.org/wiki/Application_programming_interface&#34;&gt;APIs (Application Programming Interface)&lt;/a&gt; are things heard of but seldom seen. However, whether seen or unseen, APIs are part of everyday digital life. In fact, you’ve likely used a web API from within R, even if you didn’t recognize it at the time! Several R packages are simply wrappers around popular web APIs, such as &lt;a href=&#34;https://walkerke.github.io/tidycensus/&#34;&gt;&lt;code&gt;tidycensus&lt;/code&gt;&lt;/a&gt; and &lt;a href=&#34;https://github.com/r-lib/gh&#34;&gt;&lt;code&gt;gh&lt;/code&gt;&lt;/a&gt;. Web APIs are a common framework for sharing information across a network, most commonly through &lt;a href=&#34;https://en.wikipedia.org/wiki/Hypertext_Transfer_Protocol&#34;&gt;HTTP&lt;/a&gt;.&lt;/p&gt;
&lt;div id=&#34;http&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;HTTP&lt;/h3&gt;
&lt;p&gt;To understand how HTTP requests work, it’s helpful to know the players involved. A &lt;em&gt;client&lt;/em&gt; makes a request to a &lt;em&gt;server&lt;/em&gt;, which interprets the request and provides a response. An HTTP request can be thought of simply as a packet of information sent to the server, which the server attempts to interpret and respond to. Every time you visit a URL in a web browser, an HTTP request is made and the response is rendered by the browser as the website you see. It is possible to inspect this interaction using the development tools in a browser.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-07-17-blair-plumber-intro-files/devtools-screenshot-request.png&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;As seen above, this request is composed of a URL and a request method, which in the case of a web browser accessing a website, is GET.&lt;/p&gt;
&lt;div id=&#34;request&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;Request&lt;/h4&gt;
&lt;p&gt;There are several components of an &lt;a href=&#34;https://www.w3.org/Protocols/rfc2616/rfc2616-sec5.html&#34;&gt;HTTP request&lt;/a&gt;, but here we’ll mention on only a few.&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;URL: the address or endpoint for the request&lt;/li&gt;
&lt;li&gt;Verb / method: a specific method invoked on the endpoint (GET, POST, DELETE, PUT)&lt;/li&gt;
&lt;li&gt;Headers: additional data sent to the server, such as who is making the request and what type of response is expected&lt;/li&gt;
&lt;li&gt;Body: data sent to the server outside of the headers, common for POST and PUT requests&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;In the browser example above, a GET request was made by the web browser to www.rstudio.com.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;response&#34; class=&#34;section level4&#34;&gt;
&lt;h4&gt;Response&lt;/h4&gt;
&lt;p&gt;The API response mirrors the request to some extent. It includes headers that contain information about the response and a body that contains any data returned by the API. The headers include the HTTP status code that informs the client how the request was received, along with details about the content that’s being delivered. In the example of a web browser accessing www.rstudio.com, we can see below that the response headers include the status code (200) along with details about the response content, including the fact that the content returned is HTML. This HTML content is what the browser renders into a webpage.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-07-17-blair-plumber-intro-files/devtools-screenshot-response.png&#34; /&gt;

&lt;/div&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;httr&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;httr&lt;/h3&gt;
&lt;p&gt;The &lt;a href=&#34;http://httr.r-lib.org/index.html&#34;&gt;&lt;code&gt;httr&lt;/code&gt;&lt;/a&gt; package provides a nice framework for working with HTTP requests in R. The following basic example demonstrates some of what we’ve already learned by using &lt;code&gt;httr&lt;/code&gt; and &lt;a href=&#34;http://httpbin.org/&#34;&gt;httpbin.org&lt;/a&gt;, which provides a playground of sorts for HTTP requests.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(httr)
# A simple GET request
response &amp;lt;- GET(&amp;quot;http://httpbin.org/get&amp;quot;)
response&lt;/code&gt;&lt;/pre&gt;
&lt;pre&gt;&lt;code&gt;## Response [http://httpbin.org/get]
##   Date: 2018-07-23 14:57
##   Status: 200
##   Content-Type: application/json
##   Size: 266 B
## {&amp;quot;args&amp;quot;:{},&amp;quot;headers&amp;quot;:{&amp;quot;Accept&amp;quot;:&amp;quot;application/json, text/xml, application/...&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this example we’ve made a GET request to httpbin.org/get and received a response. We know our request was successful because we see that the status is 200. We also see that the response contains data in JSON format. The &lt;a href=&#34;http://httr.r-lib.org/articles/quickstart.html&#34;&gt;&lt;em&gt;Getting started with httr&lt;/em&gt;&lt;/a&gt; page provides additional examples of working with HTTP requests and responses.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;rest&#34; class=&#34;section level3&#34;&gt;
&lt;h3&gt;REST&lt;/h3&gt;
&lt;p&gt;&lt;a href=&#34;https://en.wikipedia.org/wiki/Representational_state_transfer&#34;&gt;Representational State Transfer (REST)&lt;/a&gt; is an architectural style for APIs that includes specific constraints for building APIs to ensure that they are consistent, performant, and scalable. In order to be considered truly RESTful, an API must meet each of the following six constraints:&lt;/p&gt;
&lt;ul&gt;
&lt;li&gt;Uniform interface: clearly defined interface between client and server&lt;/li&gt;
&lt;li&gt;Stateless: state is managed via the requests themselves, not through reliance on an external service&lt;/li&gt;
&lt;li&gt;Cacheable: responses should be cacheable in order to improve scalability&lt;/li&gt;
&lt;li&gt;Client-Server: clear separation of client and server, each with it’s on distinct responsibilities in the exchange&lt;/li&gt;
&lt;li&gt;Layered System: there may be intermediaries between the client and the server, but the client should be unaware of them&lt;/li&gt;
&lt;li&gt;Code on Demand: the response can include logic executable by the client&lt;/li&gt;
&lt;/ul&gt;
&lt;p&gt;We could spend a lot of time diving further into each of these specifications, but that is beyond the scope of this post. More detail about REST can be found &lt;a href=&#34;https://www.restapitutorial.com&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;/div&gt;
&lt;div id=&#34;plumber&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Plumber&lt;/h2&gt;
&lt;p&gt;Creating RESTful APIs using R is straightforward using the &lt;code&gt;plumber&lt;/code&gt; package. Even if you have never written an API, &lt;code&gt;plumber&lt;/code&gt; makes it easy to turn existing R functions into API endpoints. Developing &lt;code&gt;plumber&lt;/code&gt; endpoints is simply a matter of providing specialized R comments before R functions. &lt;code&gt;plumber&lt;/code&gt; recognizes both &lt;code&gt;#&#39;&lt;/code&gt; and &lt;code&gt;#*&lt;/code&gt; comments, although the latter is recommended in order to avoid potential conflicts with &lt;a href=&#34;https://github.com/yihui/roxygen2&#34;&gt;&lt;code&gt;roxygen2&lt;/code&gt;&lt;/a&gt;. The following defines a &lt;code&gt;plumber&lt;/code&gt; endpoint that simply returns the data provided in the request query string.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(plumber)

#* @apiTitle Simple API

#* Echo provided text
#* @param text The text to be echoed in the response
#* @get /echo
function(text = &amp;quot;&amp;quot;) {
  list(
    message_echo = paste(&amp;quot;The text is:&amp;quot;, text)
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Here we’ve defined a simple function that takes a parameter, &lt;code&gt;text&lt;/code&gt;, and returns it with some additional comments as part of a list. By default, &lt;code&gt;plumber&lt;/code&gt; will serialize the object returned from a function into JSON using the &lt;a href=&#34;https://github.com/jeroen/jsonlite&#34;&gt;&lt;code&gt;jsonlite&lt;/code&gt;&lt;/a&gt; package. We’ve provided specialized comments to inform &lt;code&gt;plumber&lt;/code&gt; that this endpoint is available at &lt;code&gt;api-url/echo&lt;/code&gt; and will respond to GET requests.&lt;/p&gt;
&lt;p&gt;There are a few ways this &lt;code&gt;plumber&lt;/code&gt; script can be run locally. First, assuming the file is saved as &lt;code&gt;plumber.R&lt;/code&gt;, the following code would start a local web server hosting the API.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;plumber::plumb(&amp;quot;plumber.R&amp;quot;)$run(port = 5762)&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Once the web server has started, the API can be interacted with using any set of HTTP tools. We could even interact with it using &lt;code&gt;httr&lt;/code&gt; as demonstrated earlier, although we would need to open a separate R session to do so since the current R session is busy serving the API.&lt;/p&gt;
&lt;p&gt;The other method for running the API requires a recent &lt;a href=&#34;https://www.rstudio.com/products/rstudio/download/preview/&#34;&gt;preview build&lt;/a&gt; of the RStudio IDE. Recent preview builds include features that make it easier to work with &lt;code&gt;plumber&lt;/code&gt;. When editing a &lt;code&gt;plumber&lt;/code&gt; script in a recent version of the IDE, a “Run API” icon will appear in the top right hand corner of the source editor. Clicking this button will automatically run a line of code similar to the one we ran above to start a web server hosting the API. A &lt;a href=&#34;https://swagger.io&#34;&gt;swagger&lt;/a&gt;-generated UI will be rendered in the Viewer pane, and the API can be interacted with directly from within this UI.&lt;/p&gt;
&lt;div class=&#34;figure&#34;&gt;
&lt;img src=&#34;/post/2018-07-17-blair-plumber-intro-files/swagger-screenshot.png&#34; /&gt;

&lt;/div&gt;
&lt;p&gt;Now that we have a running &lt;code&gt;plumber&lt;/code&gt; API, we can query it using &lt;code&gt;curl&lt;/code&gt; from the command line to investigate it’s behavior.&lt;/p&gt;
&lt;pre class=&#34;bash&#34;&gt;&lt;code&gt;$ curl &amp;quot;localhost:5762/echo&amp;quot; | jq &amp;#39;.&amp;#39;
{
  &amp;quot;message_echo&amp;quot;: [
    &amp;quot;The text is: &amp;quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, we queried the API without providing any additional data or parameters. As a result, the &lt;code&gt;text&lt;/code&gt; parameter is the default empty string, as seen in the response. In order to pass a value to our underlying function, we can define a query string in the request as follows:&lt;/p&gt;
&lt;pre class=&#34;bash&#34;&gt;&lt;code&gt;$ curl &amp;quot;localhost:5762/echo?text=Hi%20there&amp;quot; | jq &amp;#39;.&amp;#39;
{
  &amp;quot;message_echo&amp;quot;: [
    &amp;quot;The text is: Hi there&amp;quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;In this case, the &lt;code&gt;text&lt;/code&gt; parameter is defined as part of the query string, which is appended to the end of the URL. Additional parameters could be defined by separating each key-value pair with &lt;code&gt;&amp;amp;&lt;/code&gt;. It’s also possible to pass the parameter as part of the request body. However, to leverage this method of data delivery, we need to update our API definition so that the &lt;code&gt;/echo&lt;/code&gt; endpoint also accepts POST requests. We’ll also update our API to consider multiple parameters, and return the parsed parameters along with the entire request body.&lt;/p&gt;
&lt;pre class=&#34;r&#34;&gt;&lt;code&gt;library(plumber)

#* @apiTitle Simple API

#* Echo provided text
#* @param text The text to be echoed in the response
#* @param number A number to be echoed in the response
#* @get /echo
#* @post /echo
function(req, text = &amp;quot;&amp;quot;, number = 0) {
  list(
    message_echo = paste(&amp;quot;The text is:&amp;quot;, text),
    number_echo = paste(&amp;quot;The number is:&amp;quot;, number),
    raw_body = req$postBody
  )
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;With this new API definition, the following &lt;code&gt;curl&lt;/code&gt; request can be made to pass parameters to the API via the request body.&lt;/p&gt;
&lt;pre class=&#34;bash&#34;&gt;&lt;code&gt;$ curl --data &amp;quot;text=Hi%20there&amp;amp;number=42&amp;amp;other_param=something%20else&amp;quot; &amp;quot;localhost:5762/echo&amp;quot; | jq &amp;#39;.&amp;#39;
{
  &amp;quot;message_echo&amp;quot;: [
    &amp;quot;The text is: Hi there&amp;quot;
  ],
  &amp;quot;number_echo&amp;quot;: [
    &amp;quot;The number is: 42&amp;quot;
  ],
  &amp;quot;raw_body&amp;quot;: [
    &amp;quot;text=Hi%20there&amp;amp;number=42&amp;amp;other_param=something%20else&amp;quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;Notice that we passed more than just &lt;code&gt;text&lt;/code&gt; and &lt;code&gt;number&lt;/code&gt; in the request body. &lt;code&gt;plumber&lt;/code&gt; parses the request body and matches any arguments found in the R function definition. Additional arguments, like &lt;code&gt;other_param&lt;/code&gt; in this case, are ignored. &lt;code&gt;plumber&lt;/code&gt; can parse the request body if it is URL-encoded or JSON. The following example shows the same request, but with the request body encoded as JSON.&lt;/p&gt;
&lt;pre class=&#34;bash&#34;&gt;&lt;code&gt;$ curl --data &amp;#39;{&amp;quot;text&amp;quot;:&amp;quot;Hi there&amp;quot;, &amp;quot;number&amp;quot;:&amp;quot;42&amp;quot;, &amp;quot;other_param&amp;quot;:&amp;quot;something else&amp;quot;}&amp;#39; &amp;quot;localhost:5762/echo&amp;quot; | jq &amp;#39;.&amp;#39;
{
  &amp;quot;message_echo&amp;quot;: [
    &amp;quot;The text is: Hi there&amp;quot;
  ],
  &amp;quot;number_echo&amp;quot;: [
    &amp;quot;The number is: 42&amp;quot;
  ],
  &amp;quot;raw_body&amp;quot;: [
    &amp;quot;{\&amp;quot;text\&amp;quot;:\&amp;quot;Hi there\&amp;quot;, \&amp;quot;number\&amp;quot;:\&amp;quot;42\&amp;quot;, \&amp;quot;other_param\&amp;quot;:\&amp;quot;something else\&amp;quot;}&amp;quot;
  ]
}&lt;/code&gt;&lt;/pre&gt;
&lt;p&gt;While these examples are fairly simple, they demonstrate the extraordinary facility of &lt;code&gt;plumber&lt;/code&gt;. Thanks to &lt;code&gt;plumber&lt;/code&gt;, it is now a fairly straightforward process to expose R functions so they can be consumed and leveraged by any number of systems and processes. We’ve only scratched the surface of its capabilities and, as mentioned, future posts will walk through the creation of a Slack app using &lt;code&gt;plumber&lt;/code&gt;. Comprehensive documentation for &lt;code&gt;plumber&lt;/code&gt; can be found &lt;a href=&#34;https://www.rplumber.io/docs/&#34;&gt;here&lt;/a&gt;.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;deploying&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Deploying&lt;/h2&gt;
&lt;p&gt;Up until now, we’ve just been interacting with our APIs in our local development environment. That’s great for development and testing, but when it comes time to expose an API to external services, we don’t want our laptop held responsible (at least, I don’t!). There are several &lt;a href=&#34;https://www.rplumber.io/docs/hosting.html&#34;&gt;deployment methods&lt;/a&gt; for &lt;code&gt;plumber&lt;/code&gt; outlined in the documentation. The most straightforward method of deployment is to use &lt;a href=&#34;https://www.rstudio.com/products/connect/&#34;&gt;RStudio Connect&lt;/a&gt;. When editing a &lt;code&gt;plumber&lt;/code&gt; script in recent versions of the RStudio IDE, a blue publish button will appear in the top right-hand corner of the source editor. Clicking this button brings up a menu that enables the user to publish the API to an instance of RStudio Connect. Once published, API access and performance can be configured through RStudio Connect and the API can be leveraged by external systems and processes.&lt;/p&gt;
&lt;/div&gt;
&lt;div id=&#34;conclusion&#34; class=&#34;section level2&#34;&gt;
&lt;h2&gt;Conclusion&lt;/h2&gt;
&lt;p&gt;Web APIs are a powerful mechanism for providing systematic access to computational processes. Writing APIs with &lt;code&gt;plumber&lt;/code&gt; makes it easy for others to take advantage of the work you’ve created in R without the need to rely on batch processing or code rewriting. &lt;code&gt;plumber&lt;/code&gt; is exceptionally flexible and can be used to define a wide variety of endpoints. These endpoints can be used to integrate R with other systems. As an added bonus, downstream consumers of these APIs require no knowledge of R. They only need to know how to properly interact with the API via HTTP. &lt;code&gt;plumber&lt;/code&gt; provides a convenient and reliable bridge between R and other systems and/or languages used within an organization.&lt;/p&gt;
&lt;/div&gt;

        &lt;script&gt;window.location.href=&#39;https://rviews.rstudio.com/2018/07/23/rest-apis-and-plumber/&#39;;&lt;/script&gt;
      </description>
    </item>
    
  </channel>
</rss>
