Welcome back! You’ve built a clean backend foundation:
- Routes call services
- Services call repositories
- Your validation + response formatting is consistent
The last missing piece (before we shift hard into frontend work) is persistence—because right now, restarting the server wipes everything.
In this lesson, you’ll replace “memory-only” storage with a simple but realistic persistence layer: a JSON file on disk. You’ll implement a fileTaskRepository that reads/writes data/tasks.json, wire the service layer to use it, and seed the JSON file with nicer sample tasks so your API (and soon your UI) has meaningful data to work with.
In the previous lesson, you introduced the Repository Pattern and implemented mapTaskRepository using a Map. That gave you:
- clean separation
- swap-ability
Now we’ll take advantage of that design:
- introduce a new repository implementation (
fileTaskRepository) - switch the service layer to use it
- without changing route code or API contracts
This is the final backend-focused lesson, so the goal is to make a small number of high-impact changes with tight prompts:
- Implement the file repository in one file
- Switch
taskService.tsto use it - Seed
data/tasks.jsonwith better sample tasks
A good prompt format:
Codex, modify only the files I list:
src/lib/repositories/fileTaskRepository.ts
src/lib/services/taskService.ts
data/tasks.jsonImplement file persistence using JSON exactly as described, and keep all exports and signatures unchanged.
Show full updated contents for each modified file.
The core of persistence is simple:
- read the JSON file into memory
- make a change
- write it back
In this project, the file-backed repository is implemented in src/lib/repositories/fileTaskRepository.ts.
This repository uses Node’s fs/promises for async file operations and path to create a stable file path to data/tasks.json.
FILE_PATH uses process.cwd() to anchor the path at your project root, then points to data/tasks.json. This is a simple, container-friendly way to ensure the file location is predictable.
read() is defensive: if the file is missing, invalid, or unreadable, it returns [] instead of crashing your API
That keeps your backend stable even if the data file gets wiped or corrupted during development.
nextId is recalculated after reading tasks by finding the maximum existing id. This prevents ID collisions across restarts, which is the main thing that “breaks” when you move from memory to persistence.
Now the repository exposes the same interface as your in-memory repository, but every operation reads from and writes to disk.
Every method starts from the same place: await read() so you’re always working with the latest persisted state. That’s the key difference from an in-memory store, where state “lives” in variables.
create()assigns a unique ID usingnextId++, pushes to the array, then writes the full array back to disk. It’s not the most scalable approach, but it’s perfect for learning and for small demos.update()anddelete()returnnull/falsewhen a task isn’t found, rather than throwing. That’s important because your API routes can translate those results into proper HTTP status codes (like404) consistently.
Codex, modify only
src/lib/repositories/fileTaskRepository.ts.
ImplementfileTaskRepositoryexactly as aTaskRepositorythat persists tasks indata/tasks.jsonusingfs/promises.
Keep theFILE_PATH,read(),write(), andnextIdbehavior:read()setsnextIdbased on max id and returns[]on error, andwrite()pretty-prints JSON with 2 spaces.
Do not modify any other files. Show the full updated file.
Because you already adopted the repository pattern, swapping storage is deliberately boring:
- change the repository import + assignment
- leave the service API intact
That wiring is in src/lib/services/taskService.ts.
The exported service functions keep the same names and signatures, so your API routes don’t need to change at all. This is the practical payoff of separating “business operations” from “storage mechanics.”
The only real change is const repo = fileTaskRepository;. That single line is the “switch” that moves you from ephemeral memory to persistence.
Keeping everything async ensures that callers already handle the correct shape of “real world” operations, even if the current implementation is just a JSON file.
Codex, modify only
src/lib/services/taskService.ts.
Switch the repo to usefileTaskRepository(import it and setconst repo = fileTaskRepository).
Keep the exported service function names and signatures exactly the same.
Do not modify any other files. Show the full updated file.
To make your API demo (and your upcoming frontend work) feel more realistic, seed data/tasks.json with readable titles and meaningful content. This file lives at data/tasks.json and should remain a simple JSON array.
Here’s a more presentable version based on your existing IDs and shape:
These are intentionally “UI-friendly” tasks:
- short titles
- descriptive content
- realistic due dates
This makes your frontend course feel smoother because lists and detail views look like real data.
Keeping the IDs stable helps you test:
/api/tasks/1/api/tasks/2
immediately, without needing to create tasks first.
You can later add a completed: true example if you want the filter UI to show both states without manual edits.
Codex, modify only
data/tasks.json.
Replace the placeholder titles/content with more presentable, realistic examples, keeping the same IDs and fields.
Ensure the file remains valid JSON and is formatted cleanly.
Show the full updated file.
You now have a backend that behaves like something a UI can rely on:
- Storage survives restarts via
fileTaskRepository - The service layer is stable and storage-agnostic
- The dataset is seeded with readable example tasks for UI rendering
Next course, you can go all-in on the frontend without constantly “resetting” your backend state or re-creating tasks after every run.
