# r-pkg-developer > Develop R packages following best practices with devtools, roxygen2, and testthat. Supports both tidyverse and base R coding styles. Use when creating new R packages, adding functions to packages, writing documentation, creating tests, managing dependencies, building vignettes, creating package websites with pkgdown, or preparing for CRAN submission. Triggers on R package development tasks, DESCRIPTION file editing, roxygen2 documentation, testthat testing, pkgdown website setup, or R CMD check issues. - Author: dshkol - Repository: dshkol/rpkgdevskill - Version: 20260106215415 - Stars: 0 - Forks: 0 - Last Updated: 2026-02-07 - Source: https://github.com/dshkol/rpkgdevskill - Web: https://mule.run/skillshub/@@dshkol/rpkgdevskill~r-pkg-developer:20260106215415 --- --- name: r-pkg-developer description: Develop R packages following best practices with devtools, roxygen2, and testthat. Supports both tidyverse and base R coding styles. Use when creating new R packages, adding functions to packages, writing documentation, creating tests, managing dependencies, building vignettes, creating package websites with pkgdown, or preparing for CRAN submission. Triggers on R package development tasks, DESCRIPTION file editing, roxygen2 documentation, testthat testing, pkgdown website setup, or R CMD check issues. --- # R Package Developer Build R packages following modern best practices using the devtools ecosystem. ## Package Structure ``` mypackage/ ├── DESCRIPTION # Package metadata and dependencies ├── NAMESPACE # Generated by roxygen2 - never edit manually ├── LICENSE # License file (for MIT) ├── R/ # R source files │ ├── function.R # One file per function or related functions │ └── mypackage-package.R # Package-level documentation ├── man/ # Generated documentation - never edit manually ├── tests/ │ ├── testthat.R # Test runner │ └── testthat/ # Test files (test-*.R) ├── data/ # Exported datasets (.rda) ├── data-raw/ # Data preparation scripts ├── inst/extdata/ # Raw data files (CSV, JSON, etc.) ├── vignettes/ # Long-form documentation (.Rmd) ├── _pkgdown.yml # Optional: website configuration ├── docs/ # Generated website (if building locally) ├── .Rbuildignore # Files to exclude from package └── .gitignore # Files to exclude from git ``` ## Core Development Workflow ### 1. Create Package ```r usethis::create_package("path/to/mypackage") usethis::use_git() usethis::use_mit_license("Your Name") # REQUIRED - run this immediately! ``` > **Important**: The license step is required, not optional. The DESCRIPTION template shows a placeholder - you must run `use_mit_license()` or `use_gpl3_license()` to set an actual license, or R CMD check will fail. > **Working Directory**: Most `usethis::use_*()` functions must be run from within the package directory. Either open the package as an RStudio project, or use `setwd("path/to/mypackage")` first. If creating a package inside another project directory: ```r options(usethis.allow_nested_project = TRUE) usethis::create_package("subdir/mypackage", open = FALSE) ``` ### 2. Add Functions ```r usethis::use_r("function_name") # Write function in R/function_name.R with roxygen comments ``` ### 3. Document ```r devtools::document() # Ctrl/Cmd + Shift + D # Generates man/ files and NAMESPACE ``` ### 4. Load and Test Interactively ```r devtools::load_all() # Ctrl/Cmd + Shift + L # Test functions in console ``` ### 5. Add Tests ```r usethis::use_testthat() # First time only usethis::use_test("function_name") # Write tests, then: devtools::test() # Ctrl/Cmd + Shift + T ``` ### 6. Check Package ```r devtools::check() # Ctrl/Cmd + Shift + E # Fix all ERRORs and WARNINGs ``` ### 7. Install ```r devtools::install() ``` ## Function Template ```r #' Short Title in Sentence Case #' #' Longer description of what the function does and why #' you might want to use it. #' #' @param x Description of first parameter. #' @param y Description with default. Default is `NULL`. #' #' @returns Description of return value and structure. #' #' @export #' @examples #' my_function(1, 2) #' #' # With pipe #' data |> my_function() my_function <- function(x, y = NULL) { # Implementation } ``` For internal functions (not exported), omit `@export`. ## Adding Dependencies ```r # Required dependency (Imports) usethis::use_package("dplyr") # Optional dependency (Suggests) usethis::use_package("ggplot2", type = "Suggests") ``` In code, always use explicit namespacing: ```r result <- dplyr::filter(data, x > 0) ``` ## Test Template ```r test_that("function does expected thing", { result <- my_function(input) expect_equal(result, expected) }) test_that("function handles edge cases", { expect_error(my_function(NULL), "must not be NULL") expect_equal(my_function(numeric(0)), numeric(0)) }) ``` ## Quick Reference Commands | Task | Command | Shortcut | |------|---------|----------| | Load package | `devtools::load_all()` | Ctrl/Cmd+Shift+L | | Document | `devtools::document()` | Ctrl/Cmd+Shift+D | | Test | `devtools::test()` | Ctrl/Cmd+Shift+T | | Check | `devtools::check()` | Ctrl/Cmd+Shift+E | | Install | `devtools::install()` | | | New R file | `usethis::use_r("name")` | | | New test | `usethis::use_test("name")` | | | Add dependency | `usethis::use_package("pkg")` | | | Setup website | `usethis::use_pkgdown_github_pages()` | | | View help | `?function_name` | | ## Detailed References Consult these files for comprehensive guidance on specific topics: - **DESCRIPTION file**: See [references/description.md](references/description.md) for fields, dependencies, licensing - **Documentation**: See [references/documentation.md](references/documentation.md) for roxygen2 tags and patterns - **Testing**: See [references/testing.md](references/testing.md) for testthat expectations and patterns - **Dependencies**: See [references/dependencies.md](references/dependencies.md) for Imports vs Suggests, NAMESPACE - **Data**: See [references/data.md](references/data.md) for including datasets - **Vignettes**: See [references/vignettes.md](references/vignettes.md) for long-form documentation - **Website**: See [references/pkgdown.md](references/pkgdown.md) for pkgdown setup and customization - **R CMD check**: See [references/check.md](references/check.md) for fixing errors and warnings ## Common Tasks ### Add a Vignette ```r usethis::use_vignette("getting-started") # Edit vignettes/getting-started.Rmd devtools::build_vignettes() ``` ### Add Package Data ```r usethis::use_data_raw("dataset_name") # Edit data-raw/dataset_name.R to prepare data # Document in R/data.R ``` ### Set Up GitHub ```r usethis::use_git() usethis::use_github() usethis::use_github_action_check_standard() ``` ### Add Package Website (Optional) ```r # Recommended: Auto-deploy to GitHub Pages usethis::use_pkgdown_github_pages() # Or manual setup usethis::use_pkgdown() pkgdown::build_site() # Preview locally ``` ### Prepare for CRAN ```r devtools::check(cran = TRUE) devtools::check_win_devel() spelling::spell_check_package() devtools::release() ``` ## Best Practices 1. **Document as you code** - Write roxygen comments immediately when creating functions 2. **Test early and often** - Run `devtools::test()` frequently during development 3. **Check before committing** - Run `devtools::check()` before each significant commit 4. **Use explicit namespacing** - Always `pkg::fun()` instead of importing whole packages 5. **Keep functions focused** - One function should do one thing well 6. **Minimize dependencies** - Only add packages you truly need 7. **Write helpful error messages** - Use `rlang::abort()` with clear descriptions 8. **Follow a consistent style** - Use `snake_case` for functions and variables ## Handling NSE (If Using Tidyverse) > **Skip this section if using base R.** NSE handling is only needed when your package uses dplyr, tidyr, or other tidyverse packages that use non-standard evaluation. When using dplyr/tidyr in package code, you must handle column references carefully to pass R CMD check. ### CRITICAL: Import `.data` from rlang ```r #' @importFrom rlang .data #' @export my_function <- function(df) { dplyr::filter(df, .data$column > 0) |> dplyr::mutate(new_col = .data$column * 2) } ``` > **The `@importFrom rlang .data` tag is REQUIRED.** Without it, you'll get "no visible binding for global variable" notes that fail R CMD check. Add this tag to at least one function that uses `.data`. ### When to Use Each Pattern | Pattern | Use When | Example | |---------|----------|---------| | `.data$col` | Column name is known at code time | `dplyr::filter(df, .data$status == "active")` | | `.data[[var]]` | Column name is in a variable | `dplyr::filter(df, .data[[col_name]] == value)` | | `{{ var }}` | Column passed as function argument | `dplyr::select(df, {{ user_col }})` | ### Example with User-Provided Column ```r #' @importFrom rlang .data #' @export summarize_by <- function(data, group_col, value_col) { data |> dplyr::group_by({{ group_col }}) |> dplyr::summarize(mean_val = mean({{ value_col }}, na.rm = TRUE)) } ``` Use `{{ }}` (curly-curly) when the user passes bare column names as arguments. ## Input Validation Patterns Robust functions validate inputs early and provide clear error messages. ### Basic Type Validation ```r my_function <- function(x, n = 1) { # Validate types if (!is.numeric(x)) { stop("`x` must be a numeric vector", call. = FALSE) } if (!is.numeric(n) || length(n) != 1) { stop("`n` must be a single numeric value", call. = FALSE) } # Function body... } ``` ### Common Patterns ```r # Check for NULL if (is.null(x)) { stop("`x` must not be NULL", call. = FALSE) } # Check length if (length(x) == 0) { stop("`x` must have at least one element", call. = FALSE) } # Check for NA values if (anyNA(x)) { stop("`x` must not contain NA values", call. = FALSE) } # Validate string arguments with fixed choices method <- match.arg(method) # Validates against function default values ``` ### NA-Safe Conditionals When checking conditions on vectors that might contain NA: ```r # WRONG - fails if x contains NA if (any(x == 0)) { ... } # CORRECT - handles NA safely if (any(x == 0, na.rm = TRUE)) { ... } ``` > **Important**: `any()` and `all()` return `NA` if the input contains `NA` values, which causes `if` statements to fail with "missing value where TRUE/FALSE needed". Always use `na.rm = TRUE` when the input might contain `NA`. ### Warning vs Error - **`stop()`**: Invalid inputs, impossible operations - **`warning()`**: Edge cases that produce valid but unexpected results Use `call. = FALSE` for cleaner error messages (omits the function call). ## Base R Patterns If building a minimal-dependency package, these base R patterns avoid tidyverse dependencies. ### Data Manipulation | Tidyverse | Base R Equivalent | |-----------|-------------------| | `dplyr::filter(df, x > 0)` | `df[df$x > 0, ]` or `subset(df, x > 0)` | | `dplyr::select(df, a, b)` | `df[, c("a", "b")]` | | `dplyr::mutate(df, y = x*2)` | `transform(df, y = x*2)` or `df$y <- df$x * 2` | | `purrr::map(x, fn)` | `lapply(x, fn)` | | `purrr::map_dbl(x, fn)` | `vapply(x, fn, numeric(1))` | ### String Operations ```r # Pattern matching grepl("pattern", x) gsub("old", "new", x) # Splitting strsplit(x, ",")[[1]] # Concatenation paste0("a", "b") sprintf("Value: %d", n) ``` ### Advantages of Base R - Zero external dependencies - Stable across R versions - Easier CRAN maintenance - No NSE complexity See `test-packages/fullpkg/` for an example package using base R patterns.