This chapter covers the URL Shortener API. We will look at every endpoint, how data flows, what we store, and the order in which we will build the project.
A URL shortener takes a long URL like https://dalabs.academy/courses/test-driven-development-with-nodejs/getting-started/project-setup and maps it to a short code like abc123. Anyone who visits http://localhost:3000/abc123 gets redirected to the original URL.

The behavior is easy to understand, but there is enough underlying complexity to make this a great project for learning test-driven development:
| Method | Path | Purpose |
|---|
| POST | /shorten | Create a shortened URL |
| GET | /:code | Redirect to the original URL |
| GET | /urls | List all shortened URLs |
| GET | /urls/:code/stats | Get click statistics for a URL |
| DELETE | /urls/:code | Delete a shortened URL |
Create a new short URL.
json// RequestPOST /shorten{"url": "https://dalabs.academy/courses/test-driven-development-with-nodejs"}// Response — 201 Created{"shortCode": "abc123","url": "https://dalabs.academy/courses/test-driven-development-with-nodejs","shortUrl": "http://localhost:3000/abc123"}
Redirect to the original URL. There is no JSON response here. Instead, the client receives a 302 redirect.
GET /abc123
→ 302 Redirect
Location: https://dalabs.academy/courses/test-driven-development-with-nodejs
Each time this redirect happens, we also increment the click count for that short code.
List all shortened URLs.
json// RequestGET /urls// Response — 200 OK[{"shortCode": "abc123","url": "https://dalabs.academy/courses/test-driven-development-with-nodejs","shortUrl": "http://localhost:3000/abc123","clicks": 42,"createdAt": "2026-03-17T10:00:00.000Z"}]
Get statistics for a specific short URL.
json// RequestGET /urls/abc123/stats// Response — 200 OK{"shortCode": "abc123","url": "https://dalabs.academy/courses/test-driven-development-with-nodejs","clicks": 42,"createdAt": "2026-03-17T10:00:00.000Z"}
Delete a shortened URL. This endpoint returns no body.
DELETE /urls/abc123
→ 204 No Content
If the requested code does not exist, the server returns a 404.
Two main user flows drive this system.
/shorten with a long URL.abc123 that maps to the original URL.http://localhost:3000/abc123.abc123 in storage.302 redirect to the original URL.404.
Everything else in the application, like listing URLs, viewing stats, and deleting, is standard CRUD functionality built on top of this stored data.
Every shortened URL requires four fields:
abc123).It does not matter yet whether we store this data in a plain JavaScript object, a Map, or a database row. Conceptually, these four fields will power every endpoint.
We are not going to build everything at once. Each phase introduces a bit more complexity, and the tests we write along the way will keep us safe as we make changes.
Section 2: Building the Core (where we are now). We start with POST /shorten using a hardcoded response. We will write the test, return a hardcoded value to make it pass, and then replace that fake value with real logic. Next, we will add in-memory storage using a Map and extract a service layer. By the end of this section, we will be able to shorten URLs and generate unique codes entirely in memory, without a database.
Section 3: Adding a Real Database. We will spin up PostgreSQL with Docker, set up Prisma as our ORM, and swap out the in-memory Map for real database queries. Our existing tests will tell us immediately if this migration breaks anything.
Section 4: Expanding the API. We will build the remaining endpoints: redirecting with click tracking, listing all URLs, viewing stats, and deleting. Each endpoint follows the same TDD cycle.
Section 5: Hardening & Edge Cases. We will add URL validation, handle short code collisions using PostgreSQL advisory locks, and cover the edge cases that separate a simple demo from a production service.
Section 6: Testing Infrastructure. We will look at advanced test isolation, parallel test safety, and setting up a CI pipeline with GitHub Actions. This makes the test suite itself production-grade.
This progression is intentional. We fake the behavior first, make it real, and then make it robust. Each step is small enough that you always know exactly what changed and why. Because tests from previous chapters keep running, you will know immediately if something breaks.
In the next chapter, we will write our first test for POST /shorten, fake the response to make the test pass, and see this pattern in action.