The API is mapped out. Now we build our first real endpoint, POST /shorten — but we'll make it return a fake answer on purpose.
That sounds odd, so here's the idea. Before we write any storage or code-generation logic, we want to lock down the contract: what the client sends, what comes back, and which status code. Once that contract is under test, we can fill in the real logic later without breaking anything.
So in this chapter we build a POST /shorten route that returns 201 Created and a hardcoded short code. No database. No generator. Just the shape.
Fake it till you make it. Return the simplest hardcoded answer that passes the test, then swap in real logic later. Here, the handler just returns shortCode: "abc123".
Route schema. The shape Fastify expects and returns — body is { url: string }, the 201 response is { shortCode, url, shortUrl }. Defining it once gives us validation and free Swagger docs.
app.inject(). Sends a request straight through Fastify in memory — no port, no network. Fast and reliable for tests.
201 Created. The HTTP status that means "I made a new resource," as opposed to a plain 200 OK. We pin it in the schema and assert it in the test.
The instinct is to build the generator and the storage first, then wire up the route. We're going to flip that, and it's worth knowing why.
The first question that matters is whether the contract is right: does the endpoint take a URL, return 201, and hand back the shape we agreed on in chapter 4? You don't need a database to answer that.
By faking the short code, we get the contract under test in a few lines. The test now describes exactly what a client sees — and it keeps describing that same thing as we plug in a real generator (chapter 7) and real storage (chapter 6). Neither chapter touches this test, because the contract never changes. Build storage first and you'd be testing it with no endpoint to call it through. Build the fake route first and every later chapter has a green test holding the contract steady while you swap out the internals.
Start from the red branch:
bashgit checkout 05-shorten-url-startnpm install
This test checks the HTTP contract: send a URL, get back the shape from chapter 4. It uses app.inject() to push the request through Fastify in-process.
typescript// __tests__/shorten.test.tsimport { FastifyInstance } from "fastify";import { buildApp } from "../src/app";describe("POST /shorten", () => {let app: FastifyInstance;beforeAll(async () => {app = await buildApp({ logger: false });await app.ready();});afterAll(async () => {await app.close();});it("should return 201 with a short code and short URL", async () => {const url = "https://dalabs.academy/courses/test-driven-development-with-nodejs";const response = await app.inject({method: "POST",url: "/shorten",payload: { url },});expect(response.statusCode).toBe(201);expect(response.json()).toEqual({shortCode: "abc123",url,shortUrl: "http://localhost:3000/abc123",});});});
It's the same pattern as the health check test from chapter 3: app.inject() sends the request without opening a real port, and we assert both the status code and the full JSON body. Notice the expected shortCode is "abc123" — that fake value is the contract we're pinning down.
Run it:
bashnpm test

Red. The test gets a 404, not a 201 — Fastify doesn't know about POST /shorten yet. That failure is what pulls us toward writing the route.
Switch to the finish branch to see the solution:
bashgit checkout 05-shorten-url-finish
The route defines two schemas: a request body (an object with a url field) and a 201 response (so the contract is explicit and Swagger can document it).
typescript// src/routes/shorten.tsimport { FastifyPluginAsync } from "fastify";interface ShortenRequestBody {url: string;}export const shortenRoute: FastifyPluginAsync = async (app) => {app.post<{ Body: ShortenRequestBody }>("/shorten", {schema: {description: "Create a shortened URL",tags: ["URLs"],body: {type: "object",required: ["url"],additionalProperties: false,properties: {url: { type: "string" },},},response: {201: {type: "object",required: ["shortCode", "url", "shortUrl"],properties: {shortCode: { type: "string" },url: { type: "string" },shortUrl: { type: "string" },},},},},handler: async (request, reply) => {const shortCode = "abc123";reply.code(201);return {shortCode,url: request.body.url,shortUrl: `http://localhost:3000/${shortCode}`,};},});};
No database, no generation logic. The short code is hardcoded to abc123, the handler echoes back the URL it received, and the short URL is built from that fake code. It's the smallest thing that makes the test pass.
Now register the route in app.ts:
typescript// src/app.tsimport Fastify, { FastifyInstance } from "fastify";import swagger from "@fastify/swagger";import swaggerUi from "@fastify/swagger-ui";import { healthRoute } from "./routes/health";import { shortenRoute } from "./routes/shorten";interface BuildAppOptions {logger?: boolean;}export const buildApp = async (opts: BuildAppOptions = {}): Promise<FastifyInstance> => {const app = Fastify({ logger: opts.logger ?? true });await app.register(swagger, {openapi: {info: {title: "URL Shortener API",description: "A URL shortener service built with Fastify and TDD",version: "1.0.0",},},});await app.register(swaggerUi, {routePrefix: "/documentation",});// Register routesawait app.register(healthRoute);await app.register(shortenRoute);return app;};
Run the tests again:
bashnpm test

Green. Both tests pass — the health check from chapter 3 and the new shorten endpoint. The contract is now stable, which is the whole point: we can swap the fake for real storage later without touching this test.
Because we defined schemas on the route, Swagger picks up POST /shorten automatically — just like it did for the health check in chapter 3.
Start the server:
bashnpm run dev
Open http://localhost:3000/documentation. You'll see the request body, the 201 response shape, and the endpoint description, all pulled straight from the route schema.
One schema, three payoffs: validation, type safety, and documentation.

Next we swap the hardcoded response for a real in-memory store — so the endpoint can actually remember the URLs it shortens.