Build an R Package from Scratch
Building an R package might sound like something only R experts do, but it’s actually one of the best ways to organize, share, and reuse your code. Whether you want to share a set of utility functions with colleagues or publish a tool for the R community, turning your code into a package provides structure, documentation, and reliability that scripts simply can’t match.
This guide walks you through creating a complete R package from scratch, using modern tools like devtools and roxygen2.
Why Build a Package?
Before diving in, let’s clarify why you’d want to build a package instead of just saving R scripts.
First, packages enforce a consistent structure. Your functions live in a well-organized directory with clear naming conventions. Second, packages come with built-in documentation. When you document your functions properly, users get help pages automatically. Third, packages are testable. You can include unit tests that verify your functions work correctly. Fourth, packages are installable. Anyone can install your package with a single command, whether from GitHub, CRAN, or a private repository.
Think of a package as a well-documented, versioned, and distributable version of your R scripts.
Setting Up Your Development Environment
Before building packages, you need the right tools. Install the essential packages:
install.packages(c("devtools", "roxygen2", "testthat", "pkgload"))
The devtools package provides a suite of functions for package development. Roxygen2 handles documentation generation. Testthat provides a framework for unit tests. Pkgload lets you reload your package during development without reinstalling.
You also need Rtools on Windows, which provides compilers for building packages from source. Install it from https://cran.r-project.org/bin/windows/Rtools/.
Creating the Package Skeleton
The easiest way to create a new package is with devtools::create() or usethis::create_package(). Let’s create a package called mypkg that provides some simple utility functions:
library(devtools)
create_package("mypkg")
This creates a new directory with the standard package structure:
mypkg/
├── DESCRIPTION # Package metadata
├── NAMESPACE # Exports and imports (generated)
├── R/ # Your R code goes here
├── man/ # Documentation (generated)
├── tests/ # Unit tests
└── vignettes/ # Long-form documentation
The DESCRIPTION file contains your package metadata. It defines the package name, version, description, author, license, and dependencies. The R/ directory holds your function definitions. Each file should contain related functions.
Writing Your First Functions
Let’s add some useful functions to our package. Open R/ in your package directory and create a file called utils.R:
#' Calculate the geometric mean of a numeric vector
#'
#' @param x A numeric vector with all positive values
#' @return The geometric mean as a numeric scalar
#' @examples
#' geometric_mean(c(1, 2, 3, 4, 5))
geometric_mean <- function(x) {
if (!is.numeric(x)) {
stop("x must be a numeric vector")
}
if (any(x <= 0)) {
stop("All values must be positive")
}
exp(mean(log(x)))
}
#' Scale values to a specified range
#'
#' @param x A numeric vector
#' @param min_val Minimum value of output range
#' @param max_val Maximum value of output range
#' @return Numeric vector scaled to [min_val, max_val]
#' @examples
#' scale_range(c(1, 2, 3, 4, 5), 0, 10)
scale_range <- function(x, min_val = 0, max_val = 1) {
x_range <- range(x, na.rm = TRUE)
scaled <- (x - x_range[1]) / (x_range[2] - x_range[1])
scaled * (max_val - min_val) + min_val
}
Notice the comments starting with #'. These are roxygen2 comments that get parsed into documentation. Each function should have:
- A title (first line after
#') - A description (second paragraph)
- @param descriptions for each argument
- @return description
- @examples that can be run
Documenting Your Package
Roxygen2 converts your inline comments into help pages and updates the NAMESPACE file automatically. To generate documentation:
library(devtools)
document()
This parses your roxygen2 comments and creates .Rd files in man/. It also updates NAMESPACE with export directives.
If you see export warnings, add @export to functions you want users to access:
#' @export
geometric_mean <- function(x) {
# ... function body
}
Functions without @export are internal helpers not exposed to users.
Managing Dependencies
Your package likely depends on other packages. Declare these in DESCRIPTION:
Imports:
dplyr,
ggplot2
Suggests:
testthat,
knitr
Imports lists packages required for your package to function. Suggests lists packages needed for optional features like testing or vignettes.
When your package loads, these dependencies are automatically installed if needed. Use usethis::use_package() to add dependencies safely:
usethis::use_package("dplyr")
Building and Installing
During development, use load_all() to simulate installing your package without actually installing it:
load_all(".")
This makes your functions available in your current R session, exactly as if you’d installed the package. It’s fast and perfect for iterative development.
When ready to create a proper installable package, build it:
build()
This creates a source package (.tar.gz) that can be installed on any platform. You can also build a binary:
build(binary = TRUE)
To install from your local package directory:
install(".")
Adding Tests
Testthat provides a testing framework. Initialize tests with:
usethis::use_testthat()
usethis::use_test("utils")
Write tests in tests/testthat/test-utils.R:
test_that("geometric_mean works correctly", {
expect_equal(geometric_mean(c(1, 2, 4)), 2)
expect_equal(geometric_mean(c(10, 100)), 31.62, tolerance = 0.01)
})
test_that("geometric_mean rejects non-positive values", {
expect_error(geometric_mean(c(1, -1)))
expect_error(geometric_mean(0))
})
test_that("scale_range works correctly", {
expect_equal(scale_range(c(1, 2, 3), 0, 10), c(0, 5, 10))
expect_equal(scale_range(c(5, 5, 5), 0, 1), c(0, 0, 0))
})
Run tests with:
test()
Good tests catch bugs before your users find them.
Versioning and Release
R packages use semantic versioning: major.minor.patch. Update your version in DESCRIPTION. For your first release, start with 0.1.0.
When releasing to CRAN:
- Run
R CMD checkto verify everything passes - Remove any remaining debug statements
- Ensure examples run without errors
- Test on multiple platforms if possible
- Submit with
devtools::release()
CRAN has strict standards, but the process makes your package robust.
See Also
- Writing R Packages with devtools — A deeper dive into devtools workflow
- Building and Publishing an R Package — Publishing your package to CRAN
- Reproducible Environments with renv — Managing dependencies in your package projects