Right now, the POST /shorten endpoint works, but it forgets everything the moment it responds. In this chapter, we give it a memory. We are also going to reshape the production code around it. This is our first explicit Refactor step in the Red → Green → Refactor cycle. We will change the shape of the code while keeping every test green.

Three main ideas carry this chapter.
In-memory store. Storage that lives only in the running process's memory. Here, we use a plain Map<string, string> to link a short code to a URL. It is simple and fast, but it vanishes the instant the process restarts. We will defer long-term durability to a real database later.
The seam. A place where you can swap behavior without editing the surrounding code. Our seam is the UrlStore interface (save, findByCode). The route depends on this interface instead of the concrete UrlService. Later, this allows us to drop in a database-backed store by changing just one line in app.ts.
Refactor under green tests. Changing the shape of the code without changing its behavior. We keep ourselves honest by ensuring every test stays green throughout the process. In this chapter, we add a service and an interface while shorten.test.ts continues to expect the exact same abc123 response.
We could drop a Map straight into the route handler and call it done. We avoid doing that for two reasons that will pay off later.
The first reason is testability in isolation. Storage logic has nothing to do with HTTP. Pulling it into its own class lets us test it directly. We do not need Fastify, app.inject(), or a request lifecycle. The test runs faster. When it fails, it tells us exactly what broke instead of pointing vaguely at the endpoint.
The second reason is the swap we know is coming. This course is heading toward a real database, and we want to avoid rewriting the route when we get there. By making the route depend on the UrlStore interface today, the future database migration becomes a one-line change in app.ts. We simply construct the database-backed store instead of the Map, and shorten.ts never notices. This chapter is really about 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. It uses 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.
Notice the beforeEach block. Each test gets a brand-new UrlService, so the three tests cannot contaminate each other.
Run it:
bashnpm test
The result is red. The service does not exist yet. The suite fails to run because there is no module to import. The other two suites, health and shorten, still pass. This means the failure is precisely scoped to the new behavior we have not 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. Because UrlService implements UrlStore, TypeScript enforces that the class actually satisfies the contract. The urls field is a Map<string, string> linking a short code to a URL. The save method uses set, and findByCode uses get. Calling get returns undefined for a missing key, which matches the behavior the second test asserts. The overwrite test passes automatically because calling 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 pass. The service is correct in isolation, but the route does not use it yet. Wiring it in is the refactor step.
This step is the Refactor in Red → Green → Refactor. We change the shape of the production code, updating 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. 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, meaning it owns no global state.UrlStore, the type, instead of UrlService, the class. The route depends entirely 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. The mapping is now actually persisted in memory.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;};
Notice that app.ts is the only place that names the concrete UrlService. It builds one instance and injects it using 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 will change new UrlService() here to construct the database-backed implementation instead. The shorten.ts file will not change, and shorten.test.ts will not change.
Run the full suite:
bashnpm test
The output is green. All suites pass. We have three suites and five tests: the health check, the shorten contract test, and the three new UrlService tests. The production code changed shape, adding a new service, a new injection point, and a real save call. Yet, not a single test had to be edited.

The unit test is isolated by construction. The beforeEach block builds a fresh UrlService, so the Map is empty at the start of every test.
The app-level store is different. The buildApp function creates the store once. Our shorten.test.ts file builds one app in beforeAll and reuses it across the suite. The Map therefore survives between app.inject() calls within that suite. Today, that is harmless. The route always overwrites abc123 and never reads prior state back. But it is 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.
We will defuse that trap in the Test Isolation Foundations chapter, once a real database makes leaked state dangerous. For now, just notice that the convenience and the hazard come from the exact same property.
Right now, every URL still maps to the same hardcoded abc123. Next, we will replace that fake value with a real short-code generator and make sure no two URLs ever collide.