We have our API mapped out. Now we build our first real endpoint: POST /shorten. But for now, we are going to make it return a fake answer on purpose.
Before writing any storage or code-generation logic, we need to lock down the contract. This means defining exactly what the client sends, what the server returns, and the HTTP status code. Once this contract is under test, we can safely fill in the real logic later.
In this chapter, we build a POST /shorten route that returns a 201 Created status and a hardcoded short code. No database, no generator. Just the shape of the request and response.
Fake it till you make it: Returning the simplest hardcoded answer that passes a test, then swapping in real logic later. Our handler will just return shortCode: "abc123" for now.
Route schema: The exact data shape Fastify expects and returns. Defining this once gives us automatic validation and free Swagger documentation.
app.inject(): A Fastify method that sends a request straight through the application in memory. It skips the network entirely, making tests fast and reliable.
201 Created: The HTTP status code indicating a new resource was successfully created, as opposed to a standard 200 OK.
Most developers instinctively build the generator and the database storage first, then wire up the route at the end. We are flipping that process.
The most important question right now is whether the contract is correct. Does the endpoint take a URL, return a , and hand back the exact JSON shape we agreed on in chapter 4? We do not need a database to answer that.
201By faking the short code, we get the contract under test in just a few lines of code. The test describes exactly what a client sees. It will continue to describe that same thing as we plug in real storage (chapter 6) and a real generator (chapter 7). We will not touch this test in those chapters because the contract never changes. If you build the fake route first, every later chapter has a passing 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. We send a URL and expect to get back the shape from chapter 4. It uses app.inject() to push the request through Fastify in memory.
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",});});});
This is the same pattern as the health check test from chapter 3. We use app.inject() to send the request without opening a real network port, and we assert both the status code and the full JSON body. The expected shortCode is "abc123". That fake value acts as the contract we are pinning down.
Run the test:
bashnpm test

Red. The test fails with a 404 instead of a 201 because Fastify does not know about POST /shorten yet. This failure tells us exactly what we need to build next.
Switch to the finish branch to see the solution:
bashgit checkout 05-shorten-url-finish
The route defines two schemas. The first is a request body expecting an object with a url field. The second is a 201 response. Defining these makes the contract explicit and allows Swagger to 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}`,};},});};
There is no database and no generation logic here. 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. This is the simplest possible code to make the test pass.
Now register the new 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 are working. The contract is now stable. We can safely swap the fake data for real storage later without touching this test.
Because we defined schemas directly 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 in your browser. You will see the request body, the 201 response shape, and the endpoint description. All of this is pulled straight from the route schema.
Writing the schema once gives us three benefits: 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.