Skip to contents

What is a Shiny app-package?

A Shiny app-package is an R package that also contains a Shiny application. It satisfies two requirements simultaneously:

  • It has a valid DESCRIPTION file, so R treats it as a package.
  • It contains a launch_app() function (and an app.R entry point) that runs the app.

This combination gives a Shiny app everything a package provides: proper dependency management, a documentation system, unit tests, and vignettes. etc., without any extra tooling.

File layout

movexplR6/
├── DESCRIPTION              # package identity and dependencies
├── NAMESPACE                # exported symbols (roxygen2-generated)
├── movexplR6.Rproj          # RStudio project file
├── app.R                    # thin entry point for runApp() / Run App button
├── R/
│   ├── MovieData.R          # R6 class (database + filtering)
│   ├── utils.R              # axis_vars constant
│   ├── mod_filters.R        # filter inputs module
│   ├── mod_plot.R           # scatter plot module
│   ├── movies_ui.R          # top-level UI function
│   ├── movies_server.R      # top-level server function
│   └── launch_app.R         # launch_app() entry point
├── inst/
│   └── extdata/
│       └── movies.db        # bundled SQLite database
├── tests/
│   ├── testthat.R           # test runner
│   └── testthat/
│       ├── helper.R         # shared setup (db_path)
│       ├── test-utils.R
│       ├── test-MovieData.R
│       ├── test-mod_filters.R
│       └── test-mod_plot.R
└── vignettes/
    ├── movexplR6.Rmd        # introduction and usage
    ├── r6-class.Rmd         # R6 class design
    ├── shiny-modules.Rmd    # module architecture
    └── app-package-structure.Rmd  # this vignette

DESCRIPTION

DESCRIPTION is what makes a directory an R package. For a Shiny app-package the most important fields are Imports and Suggests.

Package: movexplR6
Version: 0.0.0.9000
Type: Package
Title: Movie Explorer Shiny App-Package
Imports:
    R6,
    shiny,
    bslib,
    dplyr,
    ggplot2,
    plotly,
    DBI,
    RSQLite,
    dbplyr,
    logger
Suggests:
    testthat (>= 3.0.0),
    knitr,
    rmarkdown
Config/testthat/edition: 3
VignetteBuilder: knitr

Imports lists packages that must be installed for the app to run. devtools::install() installs them automatically. Inside package code, these packages are called with their namespace prefix (shiny::reactive(), dplyr::filter()) rather than library().

Suggests lists packages only needed for testing or building documentation. They are not installed when someone install.packages() the package.

NAMESPACE

NAMESPACE lists the symbols the package makes available to users. It is generated by devtools::document() from @export tags in the roxygen2 comments, so you should never edit it by hand.

# Generated by roxygen2: do not edit by hand

export(MovieData)
export(axis_vars)
export(launch_app)
export(mod_filters_server)
export(mod_filters_ui)
export(mod_plot_server)
export(mod_plot_ui)
export(movies_server)
export(movies_ui)

Exporting module functions and MovieData means users can call them directly, which is useful for composing the package with other packages or for testing.

The R/ directory

Each file in R/ contains one logical unit. Keeping files single-purpose makes them easier to locate and test.

File Contents
MovieData.R R6 class; database connection, data loading, filtering
utils.R axis_vars named vector shared by modules
mod_filters.R mod_filters_ui() + mod_filters_server()
mod_plot.R mod_plot_ui() + mod_plot_server()
movies_ui.R movies_ui() is the top-level layout
movies_server.R movies_server() wires modules to MovieData
launch_app.R launch_app() calls shiny::shinyApp()

No global.R, no ui.R, no server.R. Everything that was in those files in the original 051-movie-explorer example now lives in named, documented, testable functions inside R/.

inst/extdata/

Files under inst/ are copied verbatim into the installed package. extdata/ is the conventional subdirectory for external data files (the Writing R Extensions manual recommends it for non-R data).

system.file() resolves the path at runtime whether the package is installed or loaded with devtools::load_all():

db_path <- system.file("extdata/movies.db", package = "movexplR6")

This means MovieData$new() never needs a hard-coded file path.

app.R - the entry point

pkgload::load_all()
launch_app()

app.R does two things:

  1. pkgload::load_all() simulates devtools::install() in the current session, making all exported functions available without a formal install step.
  2. launch_app() calls shiny::shinyApp(ui = movies_ui(), server = movies_server).

This two-liner is the minimum needed for the RStudio Run App button and for shiny::runApp(".") to work. All real logic lives in launch_app(), not here.

launch_app()

launch_app <- function(...) {
  shiny::shinyApp(
    ui     = movies_ui(),
    server = movies_server,
    ...
  )
}

launch_app() is the package’s public API for running the app. The ... argument passes additional options to shiny::shinyApp() (e.g., options = list(port = 4321)).

Note that ui receives the result of movies_ui() (evaluated once), while server receives the function object movies_server (not called here). Shiny calls the server function once per session; the UI is built once at startup.

Tests

The tests/testthat/ directory mirrors the structure of R/. Each test file corresponds to one source file:

tests/testthat/
├── helper.R           # runs before every test file; sets db_path
├── test-utils.R       # tests for axis_vars
├── test-MovieData.R   # tests for MovieData R6 class
├── test-mod_filters.R # tests for mod_filters UI and server
└── test-mod_plot.R    # tests for mod_plot UI and server

helper.R contains setup code that testthat automatically sources before any test file runs:

db_path <- system.file("extdata/movies.db", package = "movexplR6")

Tests that need the database guard with skip_if(db_path == "", ...) so they are cleanly skipped if the package has not been installed yet (for example, in a fresh CI environment before devtools::install()).

Development workflow

The standard devtools workflow applies without modification:

# Load all R/ files into the current session (no install needed)
devtools::load_all()

# Re-generate NAMESPACE and man/ from roxygen2 tags
devtools::document()

# Run all tests in tests/testthat/
devtools::test()

# Build and install the package
devtools::install()

# Build all vignettes
devtools::build_vignettes()

# Run the app
launch_app()

The key insight is that devtools::load_all() + launch_app() gives a fast edit-reload-test loop: edit a file in R/, call load_all() in the console, and run the app again (no install required).