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.
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
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.
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.
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:
updatemerges changes and returnsnullif the task doesn’t exist- That lets the service/API layer decide whether that becomes a
404
Codex, modify only
src/lib/repositories/mapTaskRepository.ts.
ImplementmapTaskRepositoryusing aMap<number, Task>and anextIdcounter.
Ensure the object satisfies theTaskRepositoryinterface and returnsnullwhen tasks aren’t found (forgetById/update).
Do not modify any other files. Show the full updated file.
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:
getAllTaskscreateTaskgetTaskByIdupdateTaskdeleteTaskfilterTasksByStatus
This protects your API layer from churn: routes keep calling services the same way they always have.
Codex, modify only
src/lib/services/taskService.ts.
ImportmapTaskRepository, setconst 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.
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 (
Mapnow, 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”
In this lesson, you:
- Defined a repository contract in
src/lib/repositories/taskRepository.ts - Implemented an in-memory
Maprepository insrc/lib/repositories/mapTaskRepository.ts - Refactored your service layer in
src/lib/services/taskService.tsto 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
