The POST /shorten endpoint works, but it forgets everything the moment it responds. In this chapter we give it a memory and reshape the production code around it. This is our first explicit Refactor step in Red → Green → Refactor, where we change the code's shape while every test stays green.

Three ideas carry this chapter.
In-memory store. Storage that lives only in the running process's memory, here a plain Map<string, string> from short code to URL. It is simple and fast, but it vanishes the instant the process restarts, so we defer durability to a real database later.
The seam. A place where you can swap behavior without editing the code around it. Ours is the UrlStore interface (save, findByCode) that the route depends on instead of the concrete UrlService, which later lets a database-backed store drop in by touching one line in app.ts.
Refactor under green tests. Changing the shape of the code without changing its behavior, kept honest by keeping every test green throughout. Here we add a service and an interface while still asserts the same response.
shorten.test.tsabc123We could drop a Map straight into the route handler and call it done. We don't, for two reasons that pay off later in the course.
The first is testability in isolation. Storage logic ("save this, look it up, overwrite on duplicate") has nothing to do with HTTP. Pulling it into its own class lets us unit-test it directly, with no Fastify, no app.inject(), and no request lifecycle. The test is faster, and when it fails it says exactly what broke instead of pointing vaguely at "the endpoint."
The second is the swap we know is coming. The whole course is heading toward a real database, and we'd rather not rewrite the route when we get there. By having the route depend on the UrlStore interface today, the future database migration becomes a one-line change in app.ts: construct the database-backed store instead of the Map, and shorten.ts doesn't notice. What this chapter is really about is the shape of the code, not the storage technology behind it.
Start from the red branch:
bashgit checkout 06-in-memory-storage-startnpm install
The branch already has the failing test committed. It describes the storage behavior we want, against a UrlService that doesn't exist yet.
typescript// __tests__/url.service.test.tsimport { UrlService } from "../src/services/url.service";describe("UrlService", () => {let service: UrlService;beforeEach(() => {service = new UrlService();});it("stores a url and returns it when looked up by its short code", () => {service.save("abc123", "https://dalabs.academy");expect(service.findByCode("abc123")).toBe("https://dalabs.academy");});it("returns undefined for an unknown short code", () => {expect(service.findByCode("does-not-exist")).toBeUndefined();});it("overwrites the url when the same short code is saved twice", () => {service.save("abc123", "https://example.com");service.save("abc123", "https://dalabs.academy");expect(service.findByCode("abc123")).toBe("https://dalabs.academy");});});
This is a pure unit test, with no Fastify and no HTTP. It exercises the service directly through its public methods and asserts three things: a saved URL comes back by its code, an unknown code returns undefined, and saving the same code twice keeps the latest value.
Note the beforeEach: each test gets a brand-new new UrlService(), so the three tests can't contaminate each other.
Run it:
bashnpm test
Red: the service doesn't exist yet. The suite can't even run because there's no module to import. The other two suites, health and shorten, still pass, so the failure is precisely scoped to the new behavior we haven't built yet.

Switch to the finish branch to see the implementation:
bashgit checkout 06-in-memory-storage-finish
The whole service is the interface plus about ten lines of real code.
typescript// src/services/url.service.tsexport interface UrlStore {save(shortCode: string, url: string): void;findByCode(shortCode: string): string | undefined;}export class UrlService implements UrlStore {private readonly urls = new Map<string, string>();save(shortCode: string, url: string): void {this.urls.set(shortCode, url);}findByCode(shortCode: string): string | undefined {return this.urls.get(shortCode);}}
The UrlStore interface declares the two methods the route will call. UrlService implements UrlStore, so TypeScript enforces that the class actually satisfies the contract. The urls field is the Map<string, string> — short code to URL. save is a set, findByCode is a get, and get returns undefined for a missing key, which is exactly the behavior the second test asserts. The overwrite test passes for free, because Map.set on an existing key replaces the value.
Run just the new suite to confirm the service is green:
bashnpm test -- url.service
The three UrlService tests now pass. The service is correct in isolation, but the route doesn't use it yet. Wiring it in is the refactor step.
This is the part that makes it the Refactor in Red → Green → Refactor. We change the shape of the production code, both the route and the app wiring, without changing what any test expects. The existing shorten.test.ts still asserts the exact same abc123 response body, and it must stay green the whole way through.
Here's how the route depends on the store rather than owning it:
typescript// src/routes/shorten.tsimport { FastifyPluginAsync } from "fastify";import { UrlStore } from "../services/url.service";interface ShortenRequestBody {url: string;}interface ShortenRouteOptions {urlStore: UrlStore;}export const shortenRoute: FastifyPluginAsync<ShortenRouteOptions> = async (app,opts) => {const { urlStore } = opts;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 { url } = request.body;const shortCode = "abc123";urlStore.save(shortCode, url);reply.code(201);return {shortCode,url,shortUrl: `http://localhost:3000/${shortCode}`,};},});};
Three things changed from chapter 5:
FastifyPluginAsync<ShortenRouteOptions> and pulls urlStore out of opts. This is dependency injection via Fastify plugin options: the route is handed its store rather than constructing one, so it owns no global state.UrlStore, the type, not UrlService, the class. The route depends on the contract; it has no idea whether the store is a Map or a database.urlStore.save(shortCode, url) before responding. This is the one new line of behavior, and the mapping is now actually persisted.The response body is untouched. shortCode is still the hardcoded "abc123", so the contract test sees exactly what it saw before.
Now wire the store into the app. We create one UrlService in buildApp and pass it to the route:
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";import { UrlService } from "./services/url.service";interface BuildAppOptions {logger?: boolean;}export const buildApp = async (opts: BuildAppOptions = {}): Promise<FastifyInstance> => {const app = Fastify({ logger: opts.logger ?? true });const urlStore = new UrlService();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",});await app.register(healthRoute);await app.register(shortenRoute, { urlStore });return app;};
app.ts is the only place that names the concrete UrlService. It builds one instance and injects it: register(shortenRoute, { urlStore }). Because the store is created once per app and shared, every request handled by the same app instance reads and writes the same Map. One app, one store.
This is the line that matters for chapter 12. When we replace the in-memory store with a Prisma-backed one, we change new UrlService() here to construct the database-backed implementation instead. shorten.ts doesn't change, and shorten.test.ts doesn't change.
Run the full suite:
bashnpm test
Green: all suites pass. Three suites, five tests: the health check, the shorten contract test, and the three new UrlService tests. The production code changed shape, with a new service, a new injection point, and a real save, yet not a single test had to be edited.

The unit test is isolated by construction: beforeEach builds a fresh new UrlService(), so the Map is empty at the start of every test.
The app-level store is different. buildApp creates the store once, and shorten.test.ts builds one app in beforeAll and reuses it across the suite. The Map therefore survives between app.inject() calls within that suite. Today that's harmless, because the route always overwrites abc123 and never reads prior state back. But it's the seed of a real problem: in-memory state that survives between calls is convenient right up until your tests start depending on each other's leftovers.
That's the trap we defuse in the Test Isolation Foundations chapter, once a real database makes leaked state dangerous. For now, notice that the convenience and the hazard come from the same property.
Every URL still maps to the same hardcoded abc123. Next we replace that fake with a real short-code generator and make sure no two URLs ever collide.