The REST API behind Yoda, a private habit and goal tracker for a single household.
It serves three things: the fixed list of people in the household, the goals each of them has set, and the per-day tracking records of whether those goals were met. There is no authentication and no user accounts — the people are seeded by hand, because the app is only ever meant to be used by the family that runs it.
npm install
npm start # http://localhost:3001npm run dev runs the same thing under nodemon. Requires Node 20+.
| Method | Route | Purpose |
|---|---|---|
GET |
/people |
Everyone in the household |
GET |
/people/:id |
One person |
POST |
/people |
Add a person |
PUT |
/people/:id |
Replace a person |
DELETE |
/people/:id |
Remove a person |
GET |
/goals/:personId |
That person's goals |
GET |
/goals/:personId/:goalId |
One goal |
POST |
/goals/:personId |
Create a goal |
PUT |
/goals/:personId/:goalId |
Replace a goal |
DELETE |
/goals/:personId/:goalId |
Delete a goal |
GET |
/track/:personId |
That person's daily tracking records |
POST |
/track |
Record a daily goal |
PUT |
/track/:id |
Update a daily record |
PUT replaces the whole object rather than patching it; the id and personId are re-applied from the URL.
A plain routes → controllers → models stack:
index.js— mounts the three routers and enables CORS.routes/— URL shapes only.controllers/— HTTP concerns only: read params, try/catch, choose a status code.models/— all data access, viamodels/fileHelper.js.
There is no database. data/*.json is the datastore, read and written synchronously with fs. Each file wraps its rows under a single top-level key ({ "people": [...] }). monthlyGoals.json and stats.json are seeded but no route reads them yet.
Deployed on Vercel (vercel.json routes all traffic to index.js), serving https://yoda-backend.vercel.app.
Because the datastore is a set of files on disk and Vercel's serverless filesystem is ephemeral, writes do not persist in production. A POST or PUT succeeds and is visible to that instance, then disappears when the instance is recycled. In practice the deployed API is read-only, and changing the seeded content means committing an edit to data/. Moving to a real database is the prerequisite for the write endpoints being meaningful in production.
Locally, writes do persist — with the side effect that fileHelper rewrites the whole file as JSON.stringify(data, null, 2), reindenting data/*.json from 4 spaces to 2 and producing a large diff.
In models/goalsModel.js, createGoal assigns a string id (uuidv4().substr(0, 8)) and stores personId as the string from the URL, while getGoalById, updateGoalById and deleteGoalById all match using parseInt(...). A goal created through the API therefore cannot afterwards be fetched, updated or deleted by id — only the hand-seeded goals, which have numeric ids, work.
models/dailyGoalsModel.js compares personId as a string and derives new ids from length + 1, so ids collide after any deletion. The three models do not agree on id types.