In the previous chapter, we talked about building a URL Shortener API entirely through Test-Driven Development (TDD). But before we start writing any real code, let's make sure we understand what TDD actually is and why it changes the way you write software.
Test-Driven Development (TDD) is a development practice where you write a test before you write the code that makes it pass. That's it. You don't write the feature first and then add tests later. You start with the test.
This might sound backwards at first. How can you test something that doesn't exist yet? That's exactly the point. The test describes what you want the code to do. Then you write just enough code to make it work.
Most developers follow the traditional approach: write the code first, then write tests to verify it works — if they write tests at all. The problem with this approach is that tests become an afterthought. They're often skipped when deadlines are tight, and when they are written, they tend to test the implementation rather than the behavior.
TDD flips this around. The test comes first, and the code follows.
TDD follows a simple three-step cycle called Red-Green-Refactor:
Start by writing a test for the behavior you want. Run it. It should fail. If it doesn't fail, something is wrong — either the test is testing something that already works, or the test itself is broken.
A failing test is a good thing. It means you've clearly defined what "done" looks like before writing a single line of production code.
Now write the simplest code that makes the test pass. Don't overthink it. Don't optimize. Don't add features the test didn't ask for. Just make the red turn green.
This step is about correctness, not elegance. You can write ugly code here — we'll clean it up next.
With a passing test as your safety net, improve the code. Remove duplication, rename variables, extract functions — whatever makes the code cleaner. Run the tests after every change to make sure nothing breaks.
The key insight is that refactoring with tests is safe. Without tests, refactoring is gambling.
Then you repeat the cycle. Write the next failing test, make it pass, refactor. Over and over, building your application one small, tested piece at a time.

Early in my career, I knew tests were important, but I always wrote them after the fact — if I wrote them at all. The result? Tests that were hard to write, tested the wrong things, and gave me false confidence.
When I started practicing TDD, three things changed:
It catches bugs early. When you write the test first, you think about edge cases before you write the code. You're forced to ask: what should happen when the input is empty? What if the user passes a number instead of a string? These questions come up naturally when you write the test, and you handle them before they become bugs in production.
It forces better design. Code that's easy to test is usually well-designed code. If you can't write a simple test for a function, it's often because the function is doing too much. TDD pushes you toward small, focused functions with clear inputs and outputs.
It gives you confidence to refactor. Without tests, changing existing code is scary. With a solid test suite, you can refactor aggressively and know immediately if you broke something. This is the real superpower of TDD — it makes your codebase maintainable over time.
TDD isn't magic, and I want to be upfront about its costs:
The goal of this course is to get you past the learning curve by building something real, step by step.
Let's see the Red-Green-Refactor cycle in action with a simple example. We'll write a hello function that takes a name and returns a greeting.
Start by checking out the start branch:
bashgit checkout 01-what-is-tdd-start
You'll find two files:
Open hello.test.js. This is our test file:
jsconst { describe, it } = require("node:test");const assert = require("node:assert");const { hello } = require("./hello");describe("hello", () => {it("should return a greeting with the given name", () => {const result = hello("Tung");assert.strictEqual(result, "Hello, Tung");});});
A few things to notice:
node:test and node:assert — these are built into Node.js. No need to install anything. No Jest, no Mocha, no dependencies at all.describe groups related tests together.it defines a single test case — a specific behavior we expect.assert.strictEqual checks that the result exactly matches what we expect.The test says: "When I call hello('Tung'), it should return 'Hello, Tung'."
Now look at hello.js — the implementation file:
jsconst hello = (name) => {};module.exports = { hello };
The function exists, but it doesn't do anything. It returns undefined.
Run the test:
bashnode --test hello.test.js
You'll see output like this:
javascript✖ hello > should return a greeting with the given name (0.551ms)AssertionError: Expected values to be strictly equal:+ actual - expected+ undefined- 'Hello, Tung'

Red. The test fails because hello('Tung') returns undefined instead of 'Hello, Tung'. This is exactly what we want. We've defined the behavior, and now we know it doesn't work yet.
Now let's write the simplest code that makes the test pass. Update hello.js:
jsconst hello = (name) => {return `Hello, ${name}`;};module.exports = { hello };
Run the test again:
bashnode --test hello.test.js
javascript✔ hello > should return a greeting with the given name (0.505ms)ℹ tests 1ℹ suites 1ℹ pass 1ℹ fail 0

Green. The test passes. We wrote the minimum code needed — a template literal that combines "Hello, " with the name.
You can verify this by checking out the finish branch:
bashgit checkout 01-what-is-tdd-finish
The code there matches exactly what we just wrote.
In this example, there's nothing to refactor — the code is already as simple as it can be. But in real-world scenarios, this is where you'd clean up. Maybe you extracted a helper function, maybe you renamed a variable, maybe you noticed some duplication. The point is: with a passing test, you can make changes confidently.
That's the complete Red-Green-Refactor cycle. Write a failing test, make it pass, clean up. We'll follow this exact pattern throughout the entire course.
In the next chapter, we'll set up the actual project — scaffolding a Fastify application and writing our first real failing test.