11  R Package Development

A practical, end-to-end cookbook for building robust R packages with devtools, roxygen2, and testthat. Written for day‑to‑day use, from scaffolding to checks and tests.

11.1 1. Prerequisites

  • R (≥ 4.1 recommended so you have the base pipe |>)
  • RTools (Windows) / Xcode command line tools (macOS) for compilation
  • Recommended packages:
    • install.packages(c("devtools", "roxygen2", "usethis", "testthat", "pkgdown"))
# Load helpers
library(devtools)
Loading required package: usethis
library(usethis)

11.2 2. Create the Package Skeleton

11.2.1 2.1 Create a new package directory

# Creates simutils with DESCRIPTION, NAMESPACE placeholder, R/ etc.
#usethis::create_package("simutils")
# Or from an existing project folder
# usethis::create_package(getwd())

11.2.2 2.2 Initialize Git and a README

usethis::use_git()
usethis::use_readme_rmd()  # README.Rmd + badge placeholders

11.2.3 2.3 The minimal structure (what you get)

simutils/
  DESCRIPTION
  NAMESPACE
  R/
  man/

Tip: Keep all your R code inside R/. Avoid subfolders there. Group related functions in sensible files (e.g., na.R, sample.R).

11.3 3. DESCRIPTION: Package Metadata

Run helpers and then edit as needed.

usethis::use_description(fields = list(
  Title = "Simulation Utilities",
  Description = "Convenience functions for sampling and NA counting.",
  `Authors@R` = 'person(given = "Your", family = "Name", role = c("aut","cre"), email = "you@example.com")',
  License = "MIT + file LICENSE"
))

usethis::use_mit_license("Your Name")

Key fields you usually set:

  • Package, Title, Version, Description
  • Authors@R (with aut, cre roles)
  • License (e.g., MIT)
  • Depends, Imports, Suggests (use helpers below)

11.4 4. NAMESPACE: Exports and Imports (auto‑generated)

You generally do not edit NAMESPACE by hand. It’s generated from your roxygen tags in code.

  • @export marks a function for users.
  • @import, @importFrom declare your dependencies at function level.

You’ll generate it with devtools::document().

11.5 5. Write Functions + roxygen2 Headers

Create your first R file:

# File: R/na.R
#' Count NAs in a vector
#'
#' Count number of missing values (NA) in a numeric/logical/character vector.
#'
#' @param x A vector.
#' @return Integer count of NAs in `x`.
#' @examples
#' sum_na(c(1, NA, 3))
#' sum_na(airquality$Ozone)
#' @export
sum_na <- function(x) {
  sum(is.na(x))
}

Add a second exported function that works on data frames/matrices:

# File: R/na_counter.R
#' Column-wise NA counts
#'
#' Returns the number of NAs per column for a data frame or matrix.
#'
#' @param x A data frame or matrix.
#' @return A named integer vector of NA counts per column.
#' @examples
#' na_counter(airquality)
#' @export
na_counter <- function(x) {
  if (!is.data.frame(x) && !is.matrix(x)) stop("x must be a data.frame or matrix")
  colSums(is.na(x))
}

A simple sampler utility with a documented argument:

# File: R/sample_from_data.R
#' Sample rows from a data frame
#'
#' Draw a row-wise sample with optional replacement.
#'
#' @param data A data frame.
#' @param size Number of rows to sample.
#' @param replace Logical; sample with replacement?
#' @return A data frame containing the sampled rows.
#' @examples
#' sample_from_data(airquality, 10)
#' sample_from_data(airquality, 10, replace = TRUE)
#' @export
sample_from_data <- function(data, size, replace = FALSE) {
  stopifnot(is.data.frame(data))
  idx <- sample.int(nrow(data), size, replace = replace)
  data[idx, , drop = FALSE]
}

11.6 6. Generate Documentation

# Parses roxygen headers -> creates/updates NAMESPACE and man/*.Rd
devtools::document()

This will populate man/ with help files and refresh NAMESPACE.

11.8 8. Manage Dependencies

Use helpers to declare dependencies in DESCRIPTION (rather than editing by hand):

usethis::use_package("dplyr")                # add to Imports
usethis::use_package("ggplot2", type = "Suggests")

Guidelines:

  • Imports: packages needed at runtime for your functions.
  • Suggests: optional packages used in examples/tests/vignettes.
  • Depends: rarely needed; if used, it attaches packages for users (discouraged).

11.9 9. Check Your Package Thoroughly

# Full check like CRAN does; read 00check.log for details
rc <- devtools::check()
rc

Typical findings & quick fixes:

  • Missing fields (e.g., License) → add via use_mit_license() or set in DESCRIPTION.
  • Undocumented args → ensure every parameter appears in @param and usage matches.
  • Broken examples → run your examples; fix typos (e.g., airquality, not airuality).
  • Missing system tools for building PDF manual/vignettes → install LaTeX if you need PDFs.
  • Failing tests → run devtools::test(); update your code or expectations.

11.10 10. Build & Install Locally

# Build source tarball
devtools::build()

# Install the package from source
devtools::install()

# Try it out
#library(simutils)
na_counter(airquality)

11.11 11. Continuous Integration (optional)

You can wire up CI so checks run automatically on each commit. (Any provider works; pick what your org supports.)

# Example: set up a CI service config (adjust per provider)
# usethis::use_github_action_check_standard()
# or legacy: usethis::use_travis()

11.12 12. Package Website (optional)

usethis::use_pkgdown()
pkgdown::build_site()

11.13 13. Release Checklist


11.13.1 Appendix A — Common Files (at a glance)

  • DESCRIPTION: metadata & dependencies
  • NAMESPACE: auto‑generated exports/imports
  • R/: function source files
  • man/: help files (.Rd) generated by roxygen2
  • tests/: unit tests (testthat)
  • vignettes/: long‑form docs
  • data/ & R/sysdata.rda: package data

11.13.2 Appendix B — Troubleshooting Snippets

# 1) Missing License in check
usethis::use_mit_license("Your Name")

# 2) Undocumented argument in check
# -> Make sure @param includes it and re-run document()

devtools::document(); devtools::check()

# 3) Examples fail in check (typo)
# -> Fix the example code, rerun check

# 4) pdflatex not found during check
# -> Install a LaTeX distribution or run check without manual building

# 5) Dependency not available
# -> Ensure it’s in Imports/Suggests and installed in your environment

Turn this file into your team’s template. Duplicate it, search/replace simutils, and you’re ready to scaffold a new package in minutes.