Organizing Data Access with the Repository Pattern

Welcome back! You’ve already made your API feel more “real-world” by validating inputs with Zod and returning consistent JSON envelopes for both success and errors. Now we’ll tackle a different kind of polish: how your backend stores and retrieves tasks.

In this lesson, you’ll introduce the Repository Pattern so your service layer stops caring where tasks live (array, Map, database, etc.). You’ll define a repository contract, implement an in-memory Map repository, and then wire your existing service functions to use that repository—without changing your API routes.

Previously…

Last time, you standardized your API surface:

  • Requests get validated (so only safe, well-formed data flows in)
  • Responses always come back in a predictable { data | error, meta } shape

That consistency makes the API easier to consume. Now we’ll apply that same “clean boundaries” mindset to your data access:

  • Routes call services
  • Services call repositories
  • Storage details stay hidden behind a stable interface
How we’ll use Codex CLI in this lesson

This is a great “Codex CLI lesson” because the work is mostly moving logic into the right layers without inventing new features. The best prompts here are highly constrained:

  • “Modify only these files”
  • “Implement the exact interface methods”
  • “Do not change service function names/signatures”
  • “Show full updated content”

A strong example prompt looks like:

Codex, modify only src/lib/repositories/taskRepository.ts and src/lib/repositories/mapTaskRepository.ts.
Create a TaskRepository interface and implement a mapTaskRepository that stores tasks in a Map<number, Task>.
Ensure methods match the interface exactly and return the same types.
Do not modify any other files. Show full updated contents of both files.

Defining the repository contract

The repository pattern starts with a simple idea: your service layer shouldn’t depend on how data is stored—only on what operations are available. In this project, that contract lives in src/lib/repositories/taskRepository.ts.

This interface is the “shape” that all task repositories must follow, regardless of storage implementation. That means your service layer can depend on TaskRepository and stay stable even if storage changes later (e.g., a database).

Each method returns a Promise, even though an in-memory store is “instant.” This is intentional: async signatures make it painless to swap in real I/O later without rewriting every caller.

The method set mirrors the operations your API needs:

  • list
  • fetch by id
  • create
  • update
  • delete
  • filter

The repository is not business logic—it’s the lowest-level access layer for tasks.

Implementing an in-memory Map repository

Next, you create a concrete repository implementation backed by a JavaScript Map. In this project, that implementation lives in src/lib/repositories/mapTaskRepository.ts.

store is your in-memory “database”:

  • IDs are keys
  • tasks are values

Using a Map makes lookups and deletes by ID straightforward, and it avoids scanning arrays for every operation.

nextId is a simple ID generator that guarantees each created task gets a unique numeric ID during the runtime of the server. It’s not persistent across restarts (yet), but it’s perfect for learning and local development.

The repository methods match the interface exactly and focus only on storage concerns. For example:

  • update merges changes and returns null if the task doesn’t exist
  • That lets the service/API layer decide whether that becomes a 404
Codex CLI prompt you’d use for this file

Codex, modify only src/lib/repositories/mapTaskRepository.ts.
Implement mapTaskRepository using a Map<number, Task> and a nextId counter.
Ensure the object satisfies the TaskRepository interface and returns null when tasks aren’t found (for getById/update).
Do not modify any other files. Show the full updated file.

Wiring the service layer to the repository

Now we connect your existing “business-facing” functions (the service layer) to the repository. The key is that routes should not change:

  • Your API routes continue to import the same service functions
  • Those functions now delegate to the repo

This wiring lives in src/lib/services/taskService.ts.

repo is the single “switch point” for storage:

  • Today it points to mapTaskRepository
  • Later you could replace it with a database repository without touching the API routes

The service functions keep their clear, app-level names:

  • getAllTasks
  • createTask
  • getTaskById
  • updateTask
  • deleteTask
  • filterTasksByStatus

This protects your API layer from churn: routes keep calling services the same way they always have.

Codex CLI prompt you’d use for wiring

Codex, modify only src/lib/services/taskService.ts.
Import mapTaskRepository, set const repo = mapTaskRepository, and implement the exported service functions as async wrappers around the repository methods.
Keep the existing export names and signatures unchanged.
Do not modify any other files. Show the full updated file.

Why this pattern matters (even before a real database)

At this point, your app has a clean layering:

  • API routes handle HTTP specifics (request/response, status codes)
  • Services define app-level operations (task CRUD), independent of storage
  • Repositories handle storage mechanics (Map now, DB later)

That separation is what makes “future upgrades” realistic. Moving to a database becomes:

  • “write a new repository implementation”

instead of:

  • “rewrite half the codebase”
Recap and practice preview

In this lesson, you:

  • Defined a repository contract in src/lib/repositories/taskRepository.ts
  • Implemented an in-memory Map repository in src/lib/repositories/mapTaskRepository.ts
  • Refactored your service layer in src/lib/services/taskService.ts to delegate to the repository

Next, you’ll practice writing focused Codex prompts to implement and refine repository behavior—without breaking the service or API layers that depend on it. The goal is to get comfortable with “bounded changes”:

  • clean edits in one layer
  • no unpredictable ripples through the rest of the system
Sign up
Join the 1M+ learners on CodeSignal
Be a part of our community of 1M+ users who develop and demonstrate their skills on CodeSignal