The golem framework

The golem package provides many options for R programmers experienced with creating R packages, but who are looking to streamline their application development.1

opinionated framework for building production-grade Shiny applications

Getting started

To create a new golem app from the console, enter the following:

install.packages("golem")
library(golem)
golem::create_golem(path = "gap")

If creating a golem app from RStudio’s New Project Wizard, the following defaults are available:

Creating a new golem shiny app

Creating a new golem shiny app

Package files

The initial folder structure for a new golem application is below:

gap
├── DESCRIPTION
├── NAMESPACE
├── R
   ├── app_config.R
   ├── app_server.R
   ├── app_ui.R
   └── run_app.R
├── dev
   ├── 01_start.R
   ├── 02_dev.R
   ├── 03_deploy.R
   └── run_dev.R
├── gap.Rproj
├── inst
   ├── app
   │   └── www
   │       └── favicon.ico
   └── golem-config.yml
├── man
   └── run_app.Rd
├── renv
   ├── activate.R
   ├── settings.json
   └── staging
└── renv.lock

9 directories, 17 files

The dev/ folder contains golem‘s ’guided tour’ scripts. These contain functions to help guide application development.

gap/dev/
    ├── 01_start.R
    ├── 02_dev.R
    ├── 03_deploy.R
    └── run_dev.R

1 directory, 4 files
1
dev/01_start.R opens automatically
2
dev/run_dev.R is for running the ‘development version’ of the application.

Getting started

If you are familiar with R package development, you can think of the dev/ scripts as a ‘Shiny app-package development checklist.’ dev/01_start.R contains various usethis, devtools, and golem functions for for creating common package development files:

DESCRIPTION

fill_desc() uses the desc package and the sections are entered in a key = "value" format

golem::fill_desc(
  pkg_name = "gap",
  pkg_title = "A goelm app-package",
  pkg_description = "An example golem app.",
  author_first_name = "Martin",
  author_last_name = "Frigaard",
  author_email = "mjfrigaard@pm.me",
  repo_url = NULL 
)

Development dependencies

The following files are added to the root golem app-package folder:

gap/
  ├── CODE_OF_CONDUCT.md 
  ├── LICENSE 
  ├── LICENSE.md 
  ├── NEWS.md 
  └── README.Rmd 

The next golem functions in dev/01_start.R are for setting up the testing infrastructure and recommended tests.

Test suite files

tests/: Installs spelling package, includes it in the Suggests field in DESCRIPTION, adds the spelling.R to tests/, and adds recommended unit tests to tests/testthat

tests/
├── spelling.R 
└── testthat
    └── test-golem-recommended.R 

inst/: The following words will be added to the WORDLIST: Lifecycle, README, goelm, golem

inst/
└── WORDLIST 

Favicon

R/: Create R/golem_utils_ui.R and R/golem_utils_server.R utility functions in the R/ folder

R/
├── golem_utils_server.R
└── golem_utils_ui.R

with_test = TRUE: creates the associated tests in the tests/testthat folder.

tests/
└── testthat/
    ├── test-golem_utils_server.R
    └── test-golem_utils_ui.R


Both R/golem_utils_ui.R and R/golem_utils_server.R contain a lot of helper functions that come in handy if you’re tired of writing out particular function names (like reactiveValuesToList() or column(width = 6)/column(width = 12))

Check them out here:

Git

Now that we’ve reached the end of the dev/01_start.R script, we can open dev/02_dev.R and begin developing our application.

Development

The dev/02_dev.R file covers the ‘development’ phase of a new a golem app-package. Most of the golem functions in dev/02_dev.R will create files in the R/ and inst/ folders.

Application code

Before we create any new files, we’re going to dive into the code that’s included in a new golem app-package. Most of these files are in the R/ folder, but others live in inst/ and dev/.

UI and Server

The two pre-configured UI and server functions are in R/app_ui.R and R/app_server.R:

R/
├── app_config.R
├── app_server.R
├── app_ui.R
└── run_app.R

1 directory, 4 files

app_ui() and app_server() are golem-flavored UI and server files, which means they include the @noRd tag and include additional golem utilities.

App UI

app_ui <- function(request) {
  tagList(
    # Leave this function for adding external
    # resources
    golem_add_external_resources(),
    # Your application UI logic
    fluidPage(
      # Remove the line below to start
      # building your UI
      golem::golem_welcome_page() 
    )
  )
}
golem_add_external_resources <- function() {
  add_resource_path(
    "www",
    app_sys("app/www")
  )

  tags$head(
    favicon(),
    bundle_resources(
      path = app_sys("app/www"),
      app_title = "gap"
    )
    # Add here other external resources
    # for example, you can add 
    # shinyalert::useShinyalert()
  )
}

App Server

app_server <- function(input, output, session) {
  # Your application server logic
}

Configuration

The R/app_config.R file contains two functions: app_sys() and get_golem_config(). If you do some digging, you’ll find these utilities are wrappers for other package development functions:

R/app_config.R: app_sys() is a wrapper for system.file().

app_sys <- function(...) {
  system.file(..., package = "gap")
}

R/app_config.R: get_golem_config() is also included in R/app_config.R, which reads the inst/golem-config.yml configuration file.

get_golem_config <- function(
  value,
  config = Sys.getenv(
    "GOLEM_CONFIG_ACTIVE",
    Sys.getenv(
      "R_CONFIG_ACTIVE",
      "default"
    )
  ),
  use_parent = TRUE,
  # Modify this if your config file
  # is somewhere else
  file = app_sys("golem-config.yml")
) {
  config::get(
    value = value,
    config = config,
    file = file,
    use_parent = use_parent
  )
}

golem_config.yml: the inst/golem-config.yml file gives access to the app version, name, and (development) working directory, so it can be used to add “production-only elements” and is “shareable across golem projects.”

default:
  golem_name: gap
  golem_version: 0.0.0.9000
  app_prod: no
production:
  app_prod: yes
dev:
  golem_wd: !expr golem::pkg_path()

Standalone app function

run_app() is the standalone app function exported from our golem app-package.

run_app() is a wrapper around golem::with_golem_options() and shiny::shinyApp().

run_app <- function(
  onStart = NULL,
  options = list(),
  enableBookmarking = NULL,
  uiPattern = "/",
  ...
) {
  with_golem_options(
    app = shinyApp(
      ui = app_ui,
      server = app_server,
      onStart = onStart,
      options = options,
      enableBookmarking = enableBookmarking,
      uiPattern = uiPattern
    ),
    golem_opts = list(...)
  )
}

External resources

The inst/app/ and inst/app/wwww folders work just like the inst/extdata folder (they are loaded when the package is installed and makes these files available in the application at runtime).

inst/
  └── app/
        └── www/

2 directories

Application code recap


The golem framework provides a structured approach to building production-grade Shiny applications as R packages, promoting modular programming, testing, version control, and deployment readiness for enterprise or long-term projects.

UI & Server

The UI and server functions in R/app_ui.R and R/app_server.R are the entry points of the golem application, serving as wrappers that connect the app’s modules and centralize the UI layout and server logic.

Utility functions

golem’s utility functions from golem::use_utils_ui(with_test = TRUE) and golem::use_utils_server(with_test = TRUE) offer boilerplate helper functions and tests for UI and server code.

Tests

The function golem::use_recommended_tests() sets up scaffolding in tests/testthat/ to ensure your app’s UI, server logic, and utility functions work as intended. It includes examples for testing modules and behaviors, facilitating test-driven or behavior-driven development.

Configurations

The configuration files in golem apps (R/app_config.R and inst/golem-config.yml) manage environment-specific settings like API keys and database credentials. They separate app configurations from code, supporting different deployment environments (dev, staging, production).

Dependencies

dev/02_dev.R starts by dealing with the package dependencies (i.e., the packages we’ll need for our golem app to run).

Package dependencies

attachment::att_amend_desc() captures the dependencies in the R/ folder and includes them in the DESCRIPTION file.

Writing code

Building a golem application is similar to developing a regular R package, but some of the development processes have been bundled into wrapper functions. The sections below cover creating modules, utility functions, and tests:

Modules

New modules can be created with golem::add_module(). I’ve provided an example below to demonstrate the variable arguments and options:

golem::add_module(name = 'scatter_display', with_test = TRUE, export = TRUE)

The code above creates the following files:

  • R/mod_scatter_display.R is a boilerplate Shiny module (with UI and server functions):

    show/hide R/mod_name.R
    #' scatter_display UI Function
    #'
    #' @description A shiny Module.
    #'
    #' @param id,input,output,session Internal parameters for {shiny}.
    #'
    #' @rdname mod_scatter_display
    #' @export 
    #'
    #' @importFrom shiny NS tagList 
    mod_scatter_display_ui <- function(id) {
      ns <- NS(id)
      tagList(
    
      )
    }
    
    #' scatter_display Server Functions
    #'
    #' @rdname mod_scatter_display
    #' @export 
    mod_scatter_display_server <- function(id){
      moduleServer(id, function(input, output, session){
        ns <- session$ns
    
      })
    }
    
    ## To be copied in the UI
    # mod_scatter_display_ui("scatter_display_1")
    
    ## To be copied in the server
    # mod_scatter_display_server("scatter_display_1")
  • tests/testthat/test-mod_scatter_display.R: is a test file for the module with the following boilerplate tests:

    show/hide tests/testthat/test-mod_name.R
    testServer(
      mod_scatter_display_server,
      # Add here your module params
      args = list()
      , {
        ns <- session$ns
        expect_true(
          inherits(ns, "function")
        )
        expect_true(
          grepl(id, ns(""))
        )
        expect_true(
          grepl("test", ns("test"))
        )
        # Here are some examples of tests you can
        # run on your module
        # - Testing the setting of inputs
        # session$setInputs(x = 1)
        # expect_true(input$x == 1)
        # - If ever your input updates a reactiveValues
        # - Note that this reactiveValues must be passed
        # - to the testServer function via args = list()
        # expect_true(r$x == 1)
        # - Testing output
        # expect_true(inherits(output$tbl$html, "html"))
    })
    
    test_that("module ui works", {
      ui <- mod_scatter_display_ui(id = "test")
      golem::expect_shinytaglist(ui)
      # Check that formals have not been removed
      fmls <- formals(mod_scatter_display_ui)
      for (i in c("id")){
        expect_true(i %in% names(fmls))
      }
    })
  • export = TRUE exports the module functions (@export) with the name provided to name (@rdname).

The golem::add_module() function was used to add the following modules and their accompanying test files.

R/
├── mod_aes_inputs.R
├── mod_scatter_display.R
└── mod_var_inputs.R

Expand the code below to view the modules:

show/hide mod_aes_inputs.R
#' aes_inputs UI Function
#'
#' @description A shiny Module.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @rdname mod_aes_inputs_ui
#'
#' @importFrom shiny NS tagList
#'
#' @export
#'
mod_aes_inputs_ui <- function(id) {
  ns <- NS(id)
  tagList(
    sliderInput(
      inputId = ns("alpha"),
      label = "Alpha:",
      min = 0, max = 1, step = 0.1,
      value = 0.7
    ),
    sliderInput(
      inputId = ns("size"),
      label = "Size:",
      min = 0, max = 5,
      value = 3
    ),
    textInput(
      inputId = ns("plot_title"),
      label = "Plot title",
      placeholder = "Enter plot title"
    )
  )
}

#' aes_inputs Server Functions
#'
#' @rdname mod_aes_inputs_server
#'
#' @export
#'
mod_aes_inputs_server <- function(id){
  moduleServer(id, function(input, output, session){
    ns <- session$ns
    return(
      reactive({
        list(
          "alpha" = input$alpha,
          "size" = input$size,
          "plot_title" = input$plot_title
        )
      })
    )
  })
}

## To be copied in the UI
# mod_aes_inputs_ui("aes_inputs_1")

## To be copied in the server
# mod_aes_inputs_server("aes_inputs_1")
show/hide mod_var_inputs.R
#' var_inputs UI Function
#'
#' @description A shiny Module.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @rdname mod_var_inputs_ui
#'
#'
#' @importFrom shiny NS tagList
#'
#' @export
#'
mod_var_inputs_ui <- function(id) {
  ns <- NS(id)
tagList(
    selectInput(
      inputId = ns("y"),
      label = "Y-axis:",
      choices = c(
        "IMDB rating" = "imdb_rating",
        "IMDB number of votes" = "imdb_num_votes",
        "Critics Score" = "critics_score",
        "Audience Score" = "audience_score",
        "Runtime" = "runtime"
      ),
      selected = "audience_score"
    ),
    selectInput(
      inputId = ns("x"),
      label = "X-axis:",
      choices = c(
        "IMDB rating" = "imdb_rating",
        "IMDB number of votes" = "imdb_num_votes",
        "Critics Score" = "critics_score",
        "Audience Score" = "audience_score",
        "Runtime" = "runtime"
      ),
      selected = "imdb_rating"
    ),
    selectInput(
      inputId = ns("z"),
      label = "Color by:",
      choices = c(
        "Title Type" = "title_type",
        "Genre" = "genre",
        "MPAA Rating" = "mpaa_rating",
        "Critics Rating" = "critics_rating",
        "Audience Rating" = "audience_rating"
      ),
      selected = "mpaa_rating"
    )
  )
}

#' var_inputs Server Functions
#'
#' @rdname mod_var_inputs_server
#'
#' @export
#'
mod_var_inputs_server <- function(id){
  moduleServer(id, function(input, output, session){
    ns <- session$ns
    return(
      reactive({
        list(
          "y" = input$y,
          "x" = input$x,
          "z" = input$z
        )
      })
    )
  })
}

## To be copied in the UI
# mod_var_inputs_ui("var_inputs_1")

## To be copied in the server
# mod_var_inputs_server("var_inputs_1")
show/hide mod_scatter_display.R
#' scatter_display UI Function
#'
#' @description A shiny Module.
#'
#' @param id,input,output,session Internal parameters for {shiny}.
#'
#' @rdname mod_scatter_display_ui
#'
#' @importFrom shiny NS tagList
#'
#' @export
#'
mod_scatter_display_ui <- function(id) {
  ns <- NS(id)
  tagList(
    tags$br(),
    plotOutput(outputId = ns("scatterplot"))
  )
}

#' scatter_display Server Functions
#'
#' @rdname mod_scatter_display_server
#'
#'
#' @export
#'
mod_scatter_display_server <- function(id, var_inputs, aes_inputs) {
  moduleServer(id, function(input, output, session){
    ns <- session$ns

    # observe({
    # browser()

    inputs <- reactive({
      plot_title <- tools::toTitleCase(aes_inputs()$plot_title)
        list(
          x = var_inputs()$x,
          y = var_inputs()$y,
          z = var_inputs()$z,
          alpha = aes_inputs()$alpha,
          size = aes_inputs()$size,
          plot_title = plot_title

        )
    })

    output$scatterplot <- renderPlot({
      plot <- scatter_plot(
        # data --------------------
        df = movies,
        x_var = inputs()$x,
        y_var = inputs()$y,
        col_var = inputs()$z,
        alpha_var = inputs()$alpha,
        size_var = inputs()$size
      )
      plot +
        ggplot2::labs(
          title = inputs()$plot_title,
            x = stringr::str_replace_all(tools::toTitleCase(inputs()$x), "_", " "),
            y = stringr::str_replace_all(tools::toTitleCase(inputs()$y), "_", " ")
        ) +
        ggplot2::theme_minimal() +
        ggplot2::theme(legend.position = "bottom")
    })

    # })

  })
}

## To be copied in the UI
# mod_scatter_display_ui("scatter_display_1")

## To be copied in the server
# mod_scatter_display_server("scatter_display_1")

Utility functions

Utility functions can also be created directly with golem::add_utils() or golem::add_fct().2 The scatter_plot() utility function was created using the code below:

golem::add_utils(name = "scatter_display", with_test = TRUE)
  • R/utils_scatter_display.R contains the following roxygen2 documentation:

    #' scatter_display
    #'
    #' @description A utils function
    #'
    #' @return The return value, if any, from executing the utility.
    #'
    #' @noRd
    #'
  • tests/testthat/test-utils_scatter_display.R contains the following boilerplate test:

    test_that("multiplication works", {
      expect_equal(2 * 2, 4)
    })

golem::add_utils() was used to create the following utility functions:

R/
├── utils_gap_theme.R
├── utils_scatter_display.R
└── utils_tests.R

Expand the code below to view the utility functions:

show/hide utils_gap_theme.R
#' thematic golem theme
#'
#' @returns bslib theme
#'
#' @export
#'
gap_theme <- bslib::bs_theme(
  bg = "white",
  fg = "black",
  accent = "#F28E2B",
  bootswatch = "united", # optional (choose a bootstrap theme)
  primary = "#482878",
  secondary = "#F28E2B",
  success = "#59A14F",
  info = "#4E79A7",
  warning = "#F28E2B",
  danger = "#E15759",
  base_font = bslib::font_google("Ubuntu"),
  heading_font = bslib::font_google("Raleway")
)
show/hide utils_scatter_display.R
#' scatter plot utility function
#'
#' @description A utils function
#'
#' @return The return value, if any, from executing the utility.
#'
#' @rdname scatter_plot
#'
#' @importFrom rlang .data
#'
#' @export
#'
scatter_plot <- function(df, x_var, y_var, col_var, alpha_var, size_var) {
    ggplot2::ggplot(data = df,
      ggplot2::aes(x = .data[[x_var]],
          y = .data[[y_var]],
          color = .data[[col_var]])) +
      ggplot2::geom_point(alpha = alpha_var, size = size_var)
}
show/hide utils_tests.R
#' test_logger
#'
#' @description A fct function
#'
#' @param start A character string indicating the start tag. Default is `NULL`.
#' @param end A character string indicating the end tag. Default is `NULL`.
#' @param msg A character string containing the log message.
#'
#' @return The return value, if any, from executing the function.
#'
#' @rdname test_logger
#'
#' @export
#'
test_logger <- function(start = NULL, end = NULL, msg) {
  if (is.null(start) & is.null(end)) {
    cat("\n")
    cli::cli_inform("TEST:[{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] = {msg}")
  } else if (!is.null(start) & is.null(end)) {
    cat("\n")
    cli::cli_inform("\nTEST: START [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] {start} = {msg}")
  } else if (is.null(start) & !is.null(end)) {
    cat("\n")
    cli::cli_inform("\nTEST: END [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] {end} = {msg}")
  } else {
    cat("\n")
    cli::cli_inform("\nTEST: START [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}]{start} = {msg}")
    cat("\n")
    cli::cli_inform("\nTEST: END [{format(Sys.time(), '%Y-%m-%d %H:%M:%S')}] {end} = {msg}")
  }
}


Including the mod_/utils_ prefixes in the names of the .R scripts makes it easier to separate them from other code in your package namespace when using tab-completion or searching for a particular file using Ctrl + .:


Go to File/Function in RStudio

Go to File/Function in RStudio

UI

golem UI contents are placed in R/app_ui.R:

#' The application User-Interface
#'
#' @param request Internal parameter for `{shiny}`.
#'     DO NOT REMOVE.
#'
#' @import shiny
#'
#' @noRd
#'
app_ui <- function(request) {
  tagList(
    # Leave this function for adding external resources
    golem_add_external_resources(),
    # Your application UI logic
      bslib::page_fillable(
        title = "Movie Reviews (bslib)",
        theme = gap_theme,
        bslib::layout_sidebar(
          sidebar = bslib::sidebar(
            mod_var_inputs_ui("vars"),
            mod_aes_inputs_ui("aes")
          ),
          bslib::card(
            full_screen = TRUE,
            bslib::card_header(tags$h4(tags$em("Brought to you by ",
              tags$img(
                src = "www/golem.png",
                height = 100,
                width = 100,
                style = "margin:10px 10px"
                )))
              ),
            bslib::card_body(
              mod_scatter_display_ui("plot")
            )
          )
        )
      )
  )
}

We also need to update the golem_add_external_resources() function with bslib::bs_theme_dependencies() and include our theme:

#' Add external Resources to the Application
#'
#' This function is internally used to add external
#' resources inside the Shiny application.
#'
#' @import shiny
#' @importFrom golem add_resource_path activate_js favicon bundle_resources
#' @noRd
golem_add_external_resources <- function() {
  add_resource_path(
    "www",
    app_sys("app/www")
  )
  tags$head(
    favicon(),
    bundle_resources(
      path = app_sys("app/www"),
      app_title = "gap"
    ),
    # Add here other external resources
    # for example, you can add shinyalert::useShinyalert()
    bslib::bs_theme_dependencies(theme = gap_theme)
  )
}

Server

We’ll add the module server functions to R/app_server.R:

#' The application server-side
#'
#' @param input,output,session Internal parameters for {shiny}.
#'     DO NOT REMOVE.
#' @import shiny
#' @noRd
app_server <- function(input, output, session) {
  # Your application server logic

      selected_vars <- mod_var_inputs_server("vars")

      selected_aes <- mod_aes_inputs_server("aes")

      mod_scatter_display_server("plot",
                                  var_inputs = selected_vars,
                                  aes_inputs = selected_aes)

}

We’ll also include a call to thematic::thematic_shiny() in the standalone app run_app() function:

#' Run the Shiny Application
#'
#' @param ... arguments to pass to golem_opts.
#' See `?golem::get_golem_options` for more details.
#' @inheritParams shiny::shinyApp
#'
#' @export
#' @importFrom shiny shinyApp
#' @importFrom golem with_golem_options
run_app <- function(
  onStart = NULL,
  options = list(),
  enableBookmarking = NULL,
  uiPattern = "/",
  ...
) {
  thematic::thematic_shiny()
  with_golem_options(
    app = shinyApp(
      ui = app_ui,
      server = app_server,
      onStart = onStart,
      options = options,
      enableBookmarking = enableBookmarking,
      uiPattern = uiPattern
    ),
    golem_opts = list(...)
  )
}

This ensures the application will use our bslib theme with thematic.

Writing code recap


golem development enforces a standardized file structure and workflow, helping maintain consistent development practices. The helper functions encourage breaking the application into testable, reusable modules.

Adding modules

add_module() adds a .R file with a mod_ prefix (an optional test can be included with the with_test = TRUE argument)

Adding utility functions

Utility functions can be added with add_utils() or add_fct() (also includes the with_test option for tests).

Adding non-R files

  • dev/02_dev.R includes helpers for adding JavaScript, CSS, and other files to the inst/app/www/ folder:

    • golem::add_js_file()

    • golem::add_js_handler()

    • golem::add_css_file()

    • golem::add_sass_file()

    • golem::add_any_file()

Data

movies.RData is added to inst/extdata and loaded into the package with data-raw/movies.R:

## code to prepare `movies` dataset goes here
pth <- system.file('extdata/movies.RData', package = 'gap')
load(pth)
usethis::use_data(movies, overwrite = TRUE)

After calling usethis::use_data_raw('movies'), we use system.file() to locate the inst/extdata/movies.RData file with the code in data-raw/movies.R and save it in the data/ folder.

External data

inst/extdata/ contains the external data files.3

Raw data

data-raw/ contains movies.R, which is used to load extdata/movies.RData and create the data/movies.rda file.

Package data

data/ contains the movies.rda file used in the application.

inst
└── extdata
    └── movies.RData

2 directories, 1 file
data-raw
└── movies.R

1 directory, 1 file
data
└── movies.rda

1 directory, 1 file

Test files

The test suite was set up in the dev/01_start.R script with use_recommended_tests(), which is essentially a wrapper for usethis::use_testthat() with a few extras.4

Unit tests

The with_test = TRUE argument in add_module() and add_utils() ensures the modules and functions also create the accompanying test files:

tests/testthat/
          ├── test-mod_aes_inputs.R
          ├── test-mod_scatter_display.R
          ├── test-mod_var_inputs.R
          ├── test-utils_scatter_display.R
          └── test-utils_tests.R

System tests

System tests can be performed with shinytest2 (similar to non-package or non-golem apps). I’ve stored two shinytest2 example tests in vignettes/shinytest2.Rmd (the test-shinytest2 example is below).

test-shinytest2.R contains the boilerplate test from running shinytest2::record_test():

library(shinytest2)
test_that("{shinytest2} recording: gap movies app", {
  app <- AppDriver$new(
          name = "gap-movies-app", 
          height = 800, 
          width = 800, 
          timeout = 15000, 
          load_timeout = 15000)
  app$set_inputs(`vars-y` = "imdb_num_votes")
  app$set_inputs(`vars-x` = "critics_score")
  app$set_inputs(`vars-z` = "genre")
  app$set_inputs(`vars-alpha` = 0.7)
  app$set_inputs(`vars-size` = 3)
  app$set_inputs(`vars-plot_title` = "New plot title")
  app$expect_values()
})

Test coverage

Code test coverage is handled by usethis::use_coverage() and covrpage::covrpage().5

Code coverage

  • use_coverage() requires a type (“codecov” or “coveralls”).

  • The covrpage() package provides a test coverage report in tests/README.md file. This file includes a report of the R files tested, the unit test context, number of tests, and the test status.

  • The README.Rmd will include the results from devtools::check(quiet = TRUE) and a detailed report on tests coverage (see below).

The code coverage report provided by the README.Rmd file is below:

covr::package_coverage()
#> gap Coverage: 87.18%
#> R/run_app.R: 0.00%
#> R/utils_tests.R: 53.85%
#> R/golem_utils_server.R: 77.78%
#> R/mod_scatter_display.R: 78.38%
#> R/golem_utils_ui.R: 87.94%
#> R/app_config.R: 100.00%
#> R/app_server.R: 100.00%
#> R/app_ui.R: 100.00%
#> R/mod_aes_inputs.R: 100.00%
#> R/mod_var_inputs.R: 100.00%
#> R/utils_scatter_display.R: 100.00%

Deploy

The final dev/03_deploy.R script contains options for pre-deployment checks, using a Dockerfile, and deploying your app to Posit services.

Running checks

Posit products

# Launch the ShinyApp (Do not remove this comment)
# To deploy, run: rsconnect::deployApp()
# Or use the blue button on top of this file

pkgload::load_all(export_all = FALSE,
  helpers = FALSE,
  attach_testthat = FALSE)
options( "golem.app.prod" = TRUE)
gap::run_app() # add parameters here (if any)

R/_disable_autoload.R

These functions also add an empty R/_disable_autoload.R file to handle the loadSupport() warning.

Hidden files

Various hidden files are added (.rscignore) and included in the .Rbuildignore ("^rsconnect$" to ignore rsconnect folder, "^app\\.R$" to ignore the app.R file, etc.).

Deploy code

dev/03_deploy.R includes boilerplate code for deploying your application using rsconnect::deployApp().

rsconnect::deployApp(
  appName = desc::desc_get_field("Package"),
  appTitle = desc::desc_get_field("Package"),
  appFiles = c(
    # Add any additional files unique to your app here.
    "R/",
    "inst/",
    "data/",
    "NAMESPACE",
    "DESCRIPTION",
    "app.R"
  ),
  appId = rsconnect::deployments(".")$appID,
  lint = FALSE,
  forceUpdate = TRUE
)

Docker

There are two golem functions for creating Dockerfiles:

Below is an example of using add_dockerfile_with_renv():

tmp/deploy folder

deploy/
  ├── Dockerfile
  ├── Dockerfile_base
  ├── README
  ├── gap_0.0.0.9000.tar.gz
  └── renv.lock.prod

Dockerfile_base

Dockerfile_base is used to create a base image with necessary dependencies and configurations.

show/hide Dockerfile_base
FROM rocker/verse:4.4.2
RUN apt-get update -y && apt-get install -y  make zlib1g-dev git libicu-dev && rm -rf /var/lib/apt/lists/*
RUN mkdir -p /usr/local/lib/R/etc/ /usr/lib/R/etc/
RUN echo "options(renv.config.pak.enabled = FALSE, repos = c(CRAN = 'https://cran.rstudio.com/'), download.file.method = 'libcurl', Ncpus = 4)" | tee /usr/local/lib/R/etc/Rprofile.site | tee /usr/lib/R/etc/Rprofile.site
RUN R -e 'install.packages("remotes")'
RUN R -e 'remotes::install_version("renv", version = "1.0.3")'
COPY renv.lock.prod renv.lock
RUN R -e 'renv::restore()'

Dockerfile

Dockerfile builds on this base image to set up the gap app-package and its dependencies.

show/hide Dockerfile
FROM gap_base
COPY renv.lock.prod renv.lock
RUN R -e 'options(renv.config.pak.enabled = FALSE);renv::restore()'
COPY gap_*.tar.gz /app.tar.gz
RUN R -e 'remotes::install_local("/app.tar.gz",upgrade="never")'
RUN rm /app.tar.gz
EXPOSE 80
USER rstudio
CMD R -e "options('shiny.port'=80,shiny.host='0.0.0.0');library(gap);gap::run_app()"

README

README contains the commands to run in the Terminal to build and run the Docker image.

show/hide README
docker build -f Dockerfile_base --progress=plain -t gap_base .
docker build -f Dockerfile --progress=plain -t gap:latest .
docker run -p 80:80 gap:latest
# then go to 127.0.0.1:80

gap_0.0.0.9000.tar.gz

gap_0.0.0.9000.tar.gz is the app-package to deploy in the Docker container.

Dependencies

Below is a quick overview of the dependencies in gap.

NAMESPACE

The namespace file for gap is importing shiny (and the .data operator from rlang).

# Generated by roxygen2: do not edit by hand

export(run_app)
import(shiny)
importFrom(rlang,.data)

As you can see, we’re only exporting the run_app() function from gap.

Imports

The Imports field in the DESCRIPTION file lists the following:

Imports: 
    config (>= 0.3.1),
    ggplot2,
    golem (>= 0.3.5),
    rlang,
    shiny (>= 1.7.4),
    stringr,
    tools

As we can see, golem apps add golem as a dependency:

pak::local_deps_explain(
  deps = 'golem', 
  root = "_apps/gap")
ℹ Loading metadata database
✔ Loading metadata database ... done

gap -> golem

Recap

If you typically build Shiny apps in a single app.R file (or in ui.R and server.R files), the golem framework might seem overwhelming. I’ll give a quick overview of some areas I found confusing when I started using goelm:

  1. dev/ contains golem‘s ’guided tour’ scripts (01_start.R, 02_dev.R, 03_deploy.R) and run_dev.R

  2. R/: the primary app files for the UI and server are stored in the R/ folder (R/app_ui.R, R/app_server.R, R/run_app.R), as well as the configuration function (R/app_config.R)

  3. golem apps are run using the gap::run_app() function (included in the R/ folder)

  4. While developing, golem also comes with a run_dev function that reads the R/run_dev.R file and evaluates the code.

  5. The inst/ folder holds the golem-config.yml and location of any external app files.

  6. Deploying the application can be done with a single function: rsconnect::deployApp()

Generally speaking, golem’s start-up scripts save time and serve as a gentle introduction to some of the functions used in R package development.

The add_ functions are an area where golem really separates itself from standard R package development. Having dedicated Shiny development functions (and the app/inst/www folder) reduces the cognitive overhead of mapping the standard R package development functions (i.e., those from usethis and devtools) into Shiny app-package development.

golem is a popular framework for a reason–it’s designed to allow developers to build a shiny application and R package simultaneously. Added bonuses include taking advantage of RStudio’s build tools, great documentation, and user-guides..

Below is an overview of the features/functions in the golem framework:

Feature Arguments/Options Description/Comments
dev/ scripts
  • dev/01_start.R

  • dev/02_dev.R

  • dev/03_deploy.R

These files are automatically included in new golem apps and walk through set up, development, and deployment
fill_desc(): fills DESCRIPTION file Arguments are passed as strings without having to worry about formatting (i.e., utils::person()). Includes many necessary fields often overlooked when using usethis::create_package()
attachment::att_amend_desc() Updates the package dependencies in the Imports field of DESCRIPTION Although not part of the golem package, attachment is built by the fine folks at ThinkR and makes managing dependencies smoother.
set_golem_options() This sets a variety of options in the golem-config.yml file (most notably the name, version, and path to your app-package).
use_recommended_tests() Create testthat infrastructure and adds a collection of boilerplate tests in the tests/testthat/ folder.
use_utils_ui() & use_utils_server() with_test is set to TRUE Creates a collection of commonly used UI and server functions (and accompanying tests).
add_module("name", fct, utils, export, with_test)

name: ‘name’ creates mod_name_ui() and mod_name_server()

fct: creates R/mod_name_fct.R

utils: R/mod_name_utils.R

export: adds @export

with_test: creates tests/testthat/test-mod_name.R and includes boilerplate tests.

This is one of the best features in golem . A single function will create two module functions (and a module file), utility functions, and accompanying tests.

An added bonus is a consistent file naming convention.

add_fct() and add_utils() with_test: creates the accompanying tests/testthat/test-[name].R file. These are essentially wrappers for usethis::use_r() and usethis::use_test()

Adding non-R code files:

  • add_js_file("script")

  • add_js_handler("handlers")

  • add_css_file("custom")

  • add_sass_file("custom")

Each add_ function has a template:

js_template()

js_hanler_template()

css_template()

sass_template()

Each of these functions create the necessary files in the inst/app folder.

Footnotes

  1. The code used to build the golem app is here.↩︎

  2. New functions created with golem::add_*() functions are placed in the R/ folder with a @noRd tag by default (this behavior can be changed with the export argument).↩︎

  3. This contains the movies.RData file for the original Shiny application.↩︎

  4. use_recommended_tests() adds the spelling package to our DESCRIPTION and updates the WORDLIST. The tests golem creates in the tests/testthat/ folder can serve as a nice guide for users new to testthat.↩︎

  5. The covrpage package is not on CRAN, but the development version always seems to work.↩︎

  6. rhub::check_for_cran() is “deprecated and defunct”, use rhub::rhubv2() instead.↩︎