App-Package Structure
app-package-structure.RmdWhat 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
DESCRIPTIONfile, so R treats it as a package. - It contains a
launch_app()function (and anapp.Rentry 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: knitrImports 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:
-
pkgload::load_all()simulatesdevtools::install()in the current session, making all exported functions available without a formal install step. -
launch_app()callsshiny::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() 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 serverhelper.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).