Jan Hesters

The Secret To Better APIs: You're NOT Using It

Have you ever started a project full of excitement, only to find yourself tangled in a web of confusing code and unclear APIs? You're not alone.

What if I told you there's a secret method that top developers use to create better APIs and smoother projects - a method you're probably not using?

Today I'm going to share with you a little-known technique for better API design: README-Driven Development (RDD).

What is README-Driven Development (RDD)?

So, what exactly is RDD?

It's simple: you write a README before you write any code.

In this README, you describe the API of your code. You write it as if the code, the REST API, the package, the framework - whatever you're building - already exists.

Then, you share your README with your coworkers for job-related projects or with friends for side projects, so they can provide feedback. You improve the README based on their input. Repeat this process until everyone is happy with the API and all possible edge cases have been addressed.

RDD often goes hand-in-hand with Test-Driven Development (TDD). You capture your API in your tests, and then you write the code to make those tests pass.

Here's how it works:

  1. Write the API in the README as if it already exists.
  2. Get feedback from your coworkers and implement it.
  3. Capture the API by writing tests.
  4. Write the code to make the tests pass.

Why RDD?

RDD has a ton of benefits:

  • Design First Approach: It forces you to think about the inputs and the outputs first. By focusing on the API upfront, you prevent implementation details from leaking into your API design.
  • Early Feedback Loop: Writing out the API first lets your coworkers critique it and discuss technical design implications. The goal is to come up with an API that offers the best developer experience.
  • Clarity of Purpose: RDD helps to define the purpose and scope of the project from the start. This clarity reduces bugs caused by a misunderstanding of the feature being built. It can also prevent scope creep, keeping the project focused on its intended goals.
  • Avoids Redundant Work: By solidifying the API upfront, you can avoid the redundant work of rewriting code to change the API later, when changes can be more costly and complex. When you think about all edge cases early, you can avoid unnecessary rework.
  • Better Documentation: By focusing on the README first, you ensure that documentation is NOT an afterthought but a core part of the development process. This leads to better, more comprehensive documentation that benefits users and maintainers.
  • Onboarding Efficiency: A well-documented codebase with a clear README makes it easier for new team members to understand the project quickly, significantly reducing the onboarding time.
  • Enables Parallelization: When you write the documentation first, teams can work in parallel because everyone knows what to expect from each other.

In other words, RDD aligns closely with a fundamental principle in software development:

“Program to an interface, not an implementation.” - Gang of Four

What To Include In The README?

Include everything that a normal README would include:

  • Features: List the features of your project. Write down why you're creating it and what problems you're solving. This helps users - and yourself - understand what the project should be able to do. Based on these features, you can plan your API.
  • Installation Instructions: Include how to install your package or library. If your module depends on other packages, list the dependencies.
  • Usage Examples: Show how to use your package or library and what configuration options are available.
  • API Documentation: Include the API of your package or library. Make sure you cover common use cases. This is similar to the usage examples but helps you think about the API from different perspectives, like the user and the developer.
  • Contributing: Explain how to contribute to your package or library. When working on an internal development team, you can capture or challenge existing development processes.
  • License: Include the license of your package or library.

RDD Example

Let's look at an example.

Suppose you want to create a simple package that allows you to generate random integers.

Step 1: Write the README

First, you capture what your project should do and what problems its features solve.

README.md
# Random Integer Generator
 
Generate random integers with ease.
 
## Features
 
- Generate random integers between any two numbers.
- No external dependencies.

Again, this step helps you to be clear about the scope of your project.

Once you figured out what you want to do, you want to show your users how they get started and how they can use your project. So, include the missing sections like installation instructions, usage examples, API documentation, contributing, and license.

README.md
## Installation
 
```bash
npm install random-integer-generator
```
 
## Usage
 
```js
import generateRandomInteger from "random-integer-generator";
 
const randomNumber = generateRandomInteger(10, 50);
console.log(randomNumber); // Outputs a number between 10 and 50
```
 
## API
 
### `generateRandomInteger(options)`
 
- **min** (number): The minimum value (inclusive).
- **max** (number): The maximum value (inclusive).
- **Returns:** A random integer between `min` and `max`.
 
## Contributing
 
Contributions are welcome! Please open an issue or submit a pull request.
 
## License
 
This project is licensed under the MIT License.

Step 2: Get Feedback

Now, let's say you take your README to your coworkers so they can critique it.

Your coworkers point out a few ways to improve the random integer generator:

  1. You should be able to call the generateRandomInteger function without arguments.
  2. It should have a default range of 0 to 100.
  3. It should take named parameters using an options object.
  4. Lastly, it should be a named export, so you can easily add more functions in the future.

You agree with your coworkers and update the README. Your full README now looks like this:

README.md
# Random Integer Generator
 
Generate random integers with ease.
 
## Features
 
- Generate random integers between any two numbers.
- Defaults to a range between 0 and 100 if no parameters are given.
- No external dependencies.
 
## Installation
 
```bash
npm install random-integer-generator
```
 
## Usage
 
### Basic Usage
 
```js
import { generateRandomInteger } from "random-integer-generator";
 
const randomNumber = generateRandomInteger();
console.log(randomNumber); // Outputs a number between 0 and 100
```
 
### Specifying a Range
 
```js
const randomNumber = generateRandomInteger({ min: 10, max: 50 });
console.log(randomNumber); // Outputs a number between 10 and 50
```
 
## API
 
### `generateRandomInteger(options)`
 
- **options** (object, optional)
  - **min** (number, optional): The minimum value (inclusive). Defaults to 0.
  - **max** (number, optional): The maximum value (inclusive). Defaults to 100.
- **Returns:** A random integer between `min` and `max`.
 
## Contributing
 
Contributions are welcome! Please open an issue or submit a pull request.
 
## License
 
This project is licensed under the MIT License.

You show your coworkers the updated README, and they're happy with it.

Step 3: Write the Tests

Now, it's time to create your project so you can write some code.

$ mkdir random-integer-generator
$ cd random-integer-generator
$ npm init -y
Wrote to /Users/jan/random-number-generator/package.json:
 
{
  "name": "random-number-generator",
  "version": "1.0.0",
  "main": "index.js",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "description": ""
}

Add your README.md to the project.

Then install your testing library of choice. In this case, we will use Vitest.

npm install vitest --save-dev

Add a test command to your package.json.

package.json
{
  "author": "Jan Hesters",
  "description": "Generate random integers with ease.",
  "devDependencies": {
    "vitest": "2.1.1"
  },
  "keywords": [],
  "license": "ISC",
  "main": "index.js",
  "name": "random-number-generator",
  "scripts": {
    "test": "vitest"
  },
  "version": "1.0.0"
}

Now you can write your tests in index.test.js.

index.test.js
import { describe, expect, test } from 'vitest';
 
import { randomInteger } from './index.js';
 
describe('randomInteger()', () => {
  test('given a min and a max: returns a random integer between min and max', () => {
    const min = 42;
    const max = 9001;
 
    const randomNumbers = Array.from({ length: 100 }, () =>
      randomInteger({ min, max }),
    );
 
    expect(randomNumbers.every(number => number >= min)).toStrictEqual(true);
    expect(randomNumbers.every(number => number <= max)).toStrictEqual(true);
  });
 
  test('given no arguments: returns a random integer between 0 and 100', () => {
    const randomNumbers = Array.from({ length: 100 }, () => randomInteger());
 
    expect(randomNumbers.every(number => number >= 0)).toStrictEqual(true);
    expect(randomNumbers.every(number => number <= 100)).toStrictEqual(true);
  });
});

A good technique to write tests for a function with random output, is by calling it many times and checking that the results conform to the expected behavior.

Export an empty function in index.js.

index.js
export const randomInteger = () => NaN;

Now run your tests and watch them fail.

$ npm test
 
> random-number-generator@1.0.0 test
> vitest
 
 
 DEV  v2.1.1 /Users/jan/random-number-generator
 
 index.test.js (2)
 randomInteger() (2)
     × given a min and a max: returns a random integer between min and max
     × given no arguments: returns a random integer between 0 and 100
 
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯ Failed Tests 2 ⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯
 
 FAIL  index.test.js > randomInteger() > given a min and a max: returns a random integer between min and max
AssertionError: expected false to strictly equal true
 
- Expected
+ Received
 
- true
+ false
 
 index.test.js:14:58
     12|     );
     13| 
     14|     expect(randomNumbers.every(number => number >= min)).toStrictEqual(true);
       |                                                          ^
     15|     expect(randomNumbers.every(number => number <= max)).toStrictEqual(true);
     16|   });
 
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[1/2]⎯
 
 FAIL  index.test.js > randomInteger() > given no arguments: returns a random integer between 0 and 100
AssertionError: expected false to strictly equal true
 
- Expected
+ Received
 
- true
+ false
 
 index.test.js:21:56
     19|     const randomNumbers = Array.from({ length: 100 }, () => randomInteger());
     20| 
     21|     expect(randomNumbers.every(number => number >= 0)).toStrictEqual(true);
       |                                                        ^
     22|     expect(randomNumbers.every(number => number <= 100)).toStrictEqual(true);
     23|   });
 
⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯⎯[2/2]⎯
 
 Test Files  1 failed (1)
      Tests  2 failed (2)
   Start at  08:00:00
   Duration  182ms (transform 15ms, setup 0ms, collect 11ms, tests 5ms, environment 0ms, prepare 44ms)
 
 
 FAIL  Tests failed. Watching for file changes...
       press h to show help, press q to quit

You'll see that the tests fail because the function doesn't do anything yet.

Step 4: Write the Code to Make the Tests Pass

Now, it's time to implement the code that fulfills the README and passes the tests.

index.js
export const randomInteger = ({ min = 0, max = 100 } = {}) => 
  Math.floor(Math.random() * (max - min + 1)) + min;

Vitest runs in watch mode by default, so once you hit save, the tests will run again, and you can see that they pass.

 RERUN  index.js x1
 
 index.test.js (2)
 randomInteger() (2)
 given a min and a max: returns a random integer between min and max
 given no arguments: returns a random integer between 0 and 100
 
 Test Files  1 passed (1)
      Tests  2 passed (2)
   Start at  08:00:21
   Duration  8ms
 
 
 PASS  Waiting for file changes...
       press h to show help, press q to quit

When To Use RDD?

There are two main scenarios where RDD can be helpful:

1.) When there's a complex API that you need to get right, usually used by many stakeholders.

  • Open Source Projects: When you're working on a team and want to offer something as open source. For example, an SDK that connects to the service you're building.
  • Internal Libraries: If you're building an internal tool that will be used by multiple teams in your company, it's important to get the API right and receive feedback from your future users - your teammates.
  • Complex Subsystems: When working on an app or service where multiple components interact, RDD helps outline how these components will work together, promoting better understanding and consistency across the system and your team.

2.) When large cross-functional teams need to work in parallel, and you want to make sure one team isn't blocked by another.

For example, the backend team can use RDD for their API, allowing the frontend team to know exactly what requests and responses will look like.

Trade-offs

You might be asking:

"Doesn't this take more time?"

Yes, it does ... sort of.

Writing the README and asking teammates for feedback adds an extra step, making this approach initially slower than regular development.

But think about it: you'll need to discuss your code with the team anyway. So, is it really costing extra time? And why not use a streamlined, developer-friendly process? On the other hand, RDD offers a high return on investment under the right circumstances.

When working solo, there's no need for RDD unless you want to use it to plan a tool or service that you're building. Otherwise, when you're working alone and doing TDD, RDD adds little value since you already benefit from better API design by writing your tests first.

Be Careful of Scope Creep

Once you have your README for your dream API for your 10x vision that handles every edge case you and your coworkers or friends can think of, it's important to decide the order in which you'll implement features.

Think of it like building a startup: capture the big vision first, but start by building the MVP (Minimum Viable Product). Focus on the core functionality for version 1, and plan to add additional features in future releases.

Also, don't be afraid to push back if too many people want to add extra features or edge cases. It's perfectly fine to scope your project to do certain use cases well in the first version and leave the additional functionality for later.

Why You're Not Using RDD

Many developers skip RDD because:

  • They've Never Heard of It: It's less widely discussed than other methodologies.
  • Eager to Code: The excitement of coding can overshadow planning.

Now that you're in on the secret, you know when and how you can use RDD to build better APIs and drive more successful projects.

Before you start your next project, launch a new feature, or create a new library, take a moment to write the README first. It might feel a bit unusual at first, but the benefits are substantial.

Stay in flow and keep learning:

Learn senior fullstack secrets

Subscribe to my newsletter for weekly updates on new videos, articles, and courses. You'll also get exclusive bonus content and discounts.