rguides

Writing R Packages with devtools

Introduction

The devtools package provides a comprehensive workflow for R package development. It bundles tools from the tidyverse—usethis, roxygen2, testthat, and others—into a unified interface that handles the entire package lifecycle from creation to distribution.

This guide covers the standard devtools workflow: creating a package, writing functions, documenting with roxygen2, testing with testthat, and building for distribution. The examples assume R 4.0+ and current versions of devtools dependencies.

Setting up

Creating a new package

Use usethis::create_package() to initialize a new package project. This function creates the standard R package directory, DESCRIPTION and NAMESPACE files, and the R/ and tests/ directories — everything needed to start developing immediately:

# Create a new package in the current directory
usethis::create_package("mypackage")

The function creates a directory with the standard R package structure:

mypackage/
├── DESCRIPTION
├── NAMESPACE
├── R/
└── tests/

RStudio integration

If you use RStudio, create a new R Package project via File > New Project > R Package. This integrates with the devtools workflow and provides keyboard shortcuts for common tasks.

Verify your setup works:

library(devtools)
devtools::has_devel()

This checks whether you have the necessary development tools installed (R tools on Windows/macOS, or r-base-dev on Linux).

Package structure

DESCRIPTION file

The DESCRIPTION file defines package metadata. Here’s a minimal example:

Package: mypackage
Title: What the Package Does
Version: 0.1.0
Description: One paragraph describing the package.
License: MIT
Author: Your Name <your@email.com>
Maintainer: Your Name <your@email.com>
Imports: 
    dplyr,
    tidyr
Suggests: 
    testthat
Encoding: UTF-8
Language: en-US

The Imports field lists packages your package depends on at runtime. Suggests lists packages needed for testing or optional features.

NAMESPACE file

The NAMESPACE file controls what functions are exported and what other packages your code uses. With roxygen2, you typically don’t edit this manually—it gets generated from documentation comments.

R/ folder

Place all your R code files in the R/ directory. Each file should contain related functions. For example, R/data-processing.R might contain data cleaning functions, while R/visualization.R contains plotting utilities.

Writing functions

Follow these best practices for package functions:

Explicit parameter names

Always use explicit parameter names rather than relying on positional matching:

# Good
calculate_mean <- function(data, column, na.rm = FALSE) {
  mean(data[[column]], na.rm = na.rm)
}

# Avoid
calculate_mean <- function(data, col, na.rm = FALSE) {
  mean(data[[col]], na.rm = na.rm)
}

Input validation

Check inputs early and provide clear error messages. Validating arguments at the top of a function prevents confusing failures downstream, where the real error source becomes difficult to trace. Use stop() with call. = FALSE to suppress the call location from the error message:

process_data <- function(df, group_col) {
  if (!is.data.frame(df)) {
    stop("`df` must be a data frame", call. = FALSE)
  }
  
  if (!group_col %in% names(df)) {
    stop("`group_col` must be a column name in `df`", call. = FALSE)
  }
  
  # Process data...
}

Avoid global variables

Don’t rely on global variables — pass everything as parameters. Functions that read from their enclosing environment are hard to test, reason about, and reuse, because their behavior depends on state that exists outside the function signature:

# Good
transform_vector <- function(x, multiplier = 1) {
  x * multiplier
}

# Avoid - depends on external state
multiplier <- 2
transform_vector <- function(x) {
  x * multiplier
}

Documentation

roxygen2 basics

Roxygen2 lets you write documentation in your source code. Add roxygen comments above each function:

#' Process a data frame by grouping and summarizing
#'
#' @param df A data frame to process
#' @param group_var Column to group by (character string)
#' @param summary_var Column to summarize (character string)
#' @return A tibble with grouped summaries
#' @examples
#' df <- data.frame(
#'   group = c("A", "A", "B", "B"),
#'   value = c(1, 2, 3, 4)
#' )
#' process_df(df, "group", "value")
#' @export
process_df <- function(df, group_var, summary_var) {
  dplyr::group_by(df, .data[[group_var]]) |>
    dplyr::summarize(
      mean = mean(.data[[summary_var]], na.rm = TRUE),
      n = dplyr::n()
    )
}

Key roxygen2 tags: [“@param - describes function parameters”, “@return - describes what the function returns”, “@examples - runnable code examples”, “@export - makes the function available to users”, “@importFrom - imports specific functions from other packages”]

After adding roxygen comments, run:

devtools::document()

This generates .Rd files in the man/ directory and updates NAMESPACE.

Package-Level documentation

Create a file R/package-name.R with:

#' mypackage
#'
#' A short description of what the package does.
#'
#' @section Available functions:
#' \describe{
#'   \item{process_df}{Process and summarize data frames}
#' }
#' @name mypackage-package
#' @aliases mypackage
#' @keywords package
NULL

Testing

testthat basics

Create tests in tests/testthat/test-*.R files. The testing framework uses expect_*() functions:

library(testthat)

test_that("process_df groups and summarizes correctly", {
  df <- data.frame(
    group = c("A", "A", "B", "B"),
    value = c(10, 20, 30, 40)
  )
  
  result <- process_df(df, "group", "value")
  
  expect_s3_class(result, "data.frame")
  expect_equal(nrow(result), 2)
  expect_equal(result$mean, c(15, 35))
})

Running tests

Run tests during development with devtools::test(). This executes all test files in tests/testthat/ and reports failures immediately. The quick feedback loop — write code, write tests, run test() — catches bugs before they compound into harder problems:

devtools::test()
# or
testthat::test_dir("tests/testthat")

The test runner detects failures immediately, helping you catch bugs early.

Test organization

Group related tests in files:

  • test-data-processing.R - tests for data processing functions
  • test-visualization.R - tests for plotting functions

Use test_that() to create named test cases. The name should describe what you’re testing in plain language.

Building

Development workflow

Install your package in development mode to test changes:

devtools::install()

This builds and installs the package, making it available in your library. Changes to R code require reinstalling.

Use devtools::load_all() during active development to load code without a full install:

devtools::load_all(".")

Building for distribution

Build a source package:

devtools::build()

This creates a .tar.gz file you can distribute. Recipients install with:

install.packages("mypackage_0.1.0.tar.gz", repos = NULL, type = "source")

For CRAN submission, use:

devtools::check()

This runs R CMD check comprehensively, verifying your package meets CRAN standards.

Install workflow summary

The typical development cycle:

  1. Edit R code in R/ files
  2. Add roxygen2 documentation
  3. Write or update tests in tests/testthat/
  4. Run devtools::load_all() to test interactively
  5. Run devtools::test() to verify tests pass
  6. Run devtools::document() to update documentation
  7. Run devtools::install() to install the package

The devtools workflow

The core devtools workflow is: edit code → devtools::load_all() → test interactively → devtools::test()devtools::check() → commit. load_all() loads the package into the R session without installing it, making it available for interactive testing. This is much faster than install.packages('.') and is the primary development loop.

devtools::check() runs R CMD check, the comprehensive validation that CRAN runs on submitted packages. It checks code, documentation, examples, vignettes, and tests. Run it before every push and definitely before CRAN submission. Address all ERRORs and WARNINGs; NOTEs may be acceptable but should be investigated.

Documentation and NEWS

devtools::document() calls roxygen2::roxygenise() to convert roxygen comments to .Rd files and updates NAMESPACE. Always run this after adding or changing function documentation. The NAMESPACE file controls which functions are exported (visible to package users) and which are imported from other packages.

NEWS.md records user-facing changes in each version. The format ## package 1.2.0 followed by bullet points describing changes is the standard. usethis::use_news_md() creates the file, and devtools::github_links() adds GitHub changelog links. pkgdown uses NEWS.md to build the changelog page.

Dependency management

Add package dependencies in DESCRIPTION. usethis::use_package("dplyr") adds to Imports (required at runtime). usethis::use_package("ggplot2", type = "Suggests") adds to Suggests (optional, needed only for specific functionality). Never library() in package code, use import::from("package", "function") via @importFrom in roxygen comments.

Package documentation

roxygen2 generates both man/*.Rd documentation files and the NAMESPACE file from specially formatted comments in R source files. Run devtools::document() to regenerate both after changing roxygen comments. The minimum documentation for an exported function is @title, @description, @param for each argument, and @return. Use @export to add the function to the namespace.

Continuous integration

GitHub Actions with r-lib/actions provides free CI for R packages. The standard workflow runs R CMD check on Ubuntu, Windows, and macOS against the current release of R. Add r-lib/actions/check-r-package to your workflow file; it handles dependency installation, check execution, and artifact upload automatically. Failing check results are visible in the pull request before merging.

CRAN submission

Before submitting to CRAN, run devtools::check(cran = TRUE), it enables additional checks that CRAN performs. Address all NOTEs, not just WARNINGs and ERRORs, before submission; CRAN reviewers reject packages with unexplained NOTEs. Use devtools::release() for guided submission, it runs checks, prompts for confirmation of common issues, and submits to CRAN via the web form. Expect a review turnaround of 1-5 business days for initial submissions.

devtools as the development interface

devtools provides the interactive development workflow for R packages. Its functions wrap the R CMD tools that check, build, install, and test packages, making them accessible from the R console without switching to the terminal. During active development, load_all() simulates installing the package and loading it, it makes all exported and unexported functions available in the session so you can test them interactively. This is faster than a full install cycle and is the primary way to work with package code interactively.

The document() function runs roxygen2 to rebuild documentation from inline comments and updates the NAMESPACE file. Running document after any change to roxygen2 comments or after adding or removing exported functions keeps the documentation and namespace synchronized. Failing to run document after adding an @export tag is a common mistake, the function exists in the code but is not exported until the NAMESPACE is regenerated.

The package check

R CMD check is the comprehensive validation that CRAN requires. It runs your examples, runs your tests, checks documentation completeness, checks for common problems, and validates the package structure. Running check() during development catches issues that would fail CRAN submission or cause problems for package users. The output lists errors (must fix), warnings (should fix), and notes (informational).

Running check on multiple platforms catches platform-specific issues. The rhub package submits checks to r-hub’s infrastructure, which runs checks on Windows, macOS, and Linux. GitHub Actions with the standard r-lib/actions workflow runs check on all three platforms automatically on every push. Platform-specific bugs, path separator differences, locale-dependent behavior, binary package issues on Windows, only appear when checking on the affected platform.

Version management and releases

Semantic versioning for R packages follows the major.minor.patch convention. Development versions append a fourth component: 0.1.0.9000 is the development version after the 0.1.0 release. Bumping the version number is one of the last steps before a CRAN submission. usethis::use_version() bumps the version number and updates the Date field in DESCRIPTION simultaneously.

A CRAN submission starts with check() passing cleanly, followed by checking on multiple platforms, followed by running check against the current CRAN incoming checks with rhub. The CRAN submission form asks about any warnings or notes in check output. Being transparent about any remaining notes and explaining why they are not problems is better than trying to eliminate every note by any means, some notes indicate packaging patterns that CRAN accepts but check conservatively flags.

See also

  • Building R Packages, Comprehensive overview of package structure and CRAN submission
  • R Package Dependencies with renv, Managing project dependencies and reproducible environments
  • S4 Classes in R — Intermediate to advanced OOP in R packages devtools::build_vignettes() compiles vignette files to HTML or PDF before R CMD check runs — required for --as-cran checks.