This chapter covers the URL Shortener API — every endpoint, how data flows, what we store, and the order we'll build it in.
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.

Simple input, simple output, clear behavior. But underneath that simplicity is enough complexity to make it a great TDD project:
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. No JSON response — the client gets a 302 redirect.
GET /abc123
→ 302 Redirect
Location: https://dalabs.academy/courses/test-driven-development-with-nodejs
Each redirect increments 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. Returns no body.
DELETE /urls/abc123
→ 204 No Content
If the code doesn't exist, this returns a 404.
Two flows drive the system.
/shorten with a long URLabc123 that maps to the original URLhttp://localhost:3000/abc123abc123 in storage302 redirect to the original URL404
Everything else — listing URLs, viewing stats, deleting — is CRUD on top of the same stored data.
Every shortened URL needs four fields:
abc123)Whether we store this in a JavaScript object, a Map, or a database row doesn't matter yet. These four fields power every endpoint.
We won't build everything at once. Each phase introduces new complexity while tests from the previous phase keep us safe.
Section 2: Building the Core (where we are now) — Start with POST /shorten using a hardcoded response. Write the test, return a hardcoded value to make it green, then replace the fake with real logic. We'll add in-memory storage with a Map, then extract a service layer. By the end, we can shorten URLs and generate unique codes — all in memory, no database.
Section 3: Adding a Real Database — Spin up PostgreSQL with Docker, set up Prisma as our ORM, and swap out the in-memory Map for real database queries. The existing tests tell us immediately if the migration breaks anything.
Section 4: Expanding the API — Build the remaining endpoints: redirect with click tracking, list all URLs, stats, and delete. Each one follows the same TDD cycle. By now the pattern is second nature.
Section 5: Hardening & Edge Cases — Add URL validation, handle short code collisions with PostgreSQL advisory locks, and cover the edge cases that separate a demo from a production service.
Section 6: Testing Infrastructure — Advanced test isolation, parallel test safety, and a CI pipeline with GitHub Actions. Making the test suite itself production-grade.
The progression is intentional: fake it, then make it real, then make it robust. Each step is small enough that you always know what changed and why. Tests from previous chapters keep running — if something breaks, you know immediately.
Next, we'll write the first test for POST /shorten, fake the response to make it green, and see this pattern in action.
| 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 |