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.tsandsrc/lib/repositories/mapTaskRepository.ts.
Create aTaskRepositoryinterface and implement amapTaskRepositorythat stores tasks in aMap<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.
