Writing R Packages with devtools

· 5 min read · Updated March 12, 2026 · intermediate
r r-package devtools cran

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:

  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

See Also