Repository Pattern Basics

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.

// src/lib/repositories/taskRepository.ts
import type { Task } from '@/lib/tasks';

export interface TaskRepository {
  getAll(): Promise<Task[]>;
  getById(id: number): Promise<Task | null>;
  create(data: Omit<Task, 'id'>): Promise<Task>;
  update(id: number, updates: Partial<Task>): Promise<Task | null>;
  delete(id: number): Promise<boolean>;
  filterByCompletion(status: boolean): Promise<Task[]>;
}

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.

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