File Persistence with JSON: Making Tasks Survive Server Restarts

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.

Previously…

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
How we’ll use Codex CLI in this lesson

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.ts to use it
  • Seed data/tasks.json with 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.json

Implement file persistence using JSON exactly as described, and keep all exports and signatures unchanged.
Show full updated contents for each modified file.

Persisting tasks with a file-backed repository

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.

Reading and writing the JSON file

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.

Implementing the repository methods

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 using nextId++, 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() and delete() return null / false when a task isn’t found, rather than throwing. That’s important because your API routes can translate those results into proper HTTP status codes (like 404) consistently.
Codex CLI prompt you’d use for this file

Codex, modify only src/lib/repositories/fileTaskRepository.ts.
Implement fileTaskRepository exactly as a TaskRepository that persists tasks in data/tasks.json using fs/promises.
Keep the FILE_PATH, read(), write(), and nextId behavior: read() sets nextId based on max id and returns [] on error, and write() pretty-prints JSON with 2 spaces.
Do not modify any other files. Show the full updated file.

Switching the service layer to file persistence

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 CLI prompt you’d use here

Codex, modify only src/lib/services/taskService.ts.
Switch the repo to use fileTaskRepository (import it and set const repo = fileTaskRepository).
Keep the exported service function names and signatures exactly the same.
Do not modify any other files. Show the full updated file.

Seeding data/tasks.json with nicer sample tasks

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 CLI prompt you’d use for seeding

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.

Recap: why this is the perfect “handoff” to frontend work

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.

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