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 sets up the required directory structure and files:
# 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:
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:
# 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()
# 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:
- Edit R code in R/ files
- Add roxygen2 documentation
- Write or update tests in tests/testthat/
- Run
devtools::load_all()to test interactively - Run
devtools::test()to verify tests pass - Run
devtools::document()to update documentation - Run
devtools::install()to install the package
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