HCDD 411
1 / 20

Let's Build a Backend

Md Touhidul Islam

Goal by the end:
  • A small REST API: /notes
  • Data stored in data/notes.json (no database)

Step 0: Project folder structure

✅ Create these folders/files

  • backend-notes/
  • backend-notes/src/server.mjs
  • backend-notes/data/notes.json
  • backend-notes/package.json

✅ What is this API?

  • GET /health → quick server check
  • GET /notes → list notes
  • POST /notes → create
  • GET /notes/:id → read one
  • PUT /notes/:id → update
  • DELETE /notes/:id → delete
Why JSON file? It forces us to learn how backends do persistence: validate input → read current state → modify → write back atomically → return response.

Step 1: package.json + scripts

COPY-PASTE: package.json
📄 package.json
Ln 1, Col 1JSON
Run these in terminal:
  • npm install
  • npm run dev

Step 2: Create the first server (health route)

COPY-PASTE: src/server.mjs (v1)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed (vs nothing):
  • We created one endpoint: GET /health
  • We return a JSON response with status 200
  • Unknown routes return 404
From now on: every step should keep /health working. If it breaks, rollback and fix.
Test:
  • curl http://localhost:3000/health
  • Expected: {"ok":true}

Step 3: Add response helpers (consistent JSON)

COPY-PASTE: src/server.mjs (v2)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v1 (look for highlighted lines):
  • json(res, status, payload) — one function to set JSON headers + stringify
  • notFound(res) — one place for the standard 404 response
  • Benefit: future routes stay small and readable (no repeated boilerplate)
In the editor, any line tagged with // NEW / // CHANGED is highlighted.

Step 4: Read request body (POST needs it)

COPY-PASTE: src/server.mjs (v3)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v2:
  • readBody(req) added to read streaming request bodies
  • new URL(req.url, ...) added (clean path parsing)
  • New practice route: POST /echo returns the JSON you sent
Test:
  • curl -X POST http://localhost:3000/echo -H "Content-Type: application/json" -d '{"msg":"hi"}'
  • Expected: {"received":{"msg":"hi"}}

Step 5: Create the JSON “database” file

Start with an empty array. Every note will be an object with id, text, createdAt, updatedAt.

COPY-PASTE: data/notes.json
📄 data/notes.json
Ln 1, Col 1JSON
Important: This “JSON file DB” is good for learning, not production. Concurrency is tricky (two requests writing at the same time). We’ll still build it correctly for class.

Step 6: Load + save notes from the JSON file

COPY-PASTE: src/server.mjs (v4)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v3:
  • Added loadNotes() and saveNotes() using fs/promises
  • Added NOTES_PATH built from import.meta.url so it works from any working directory
  • Added first real resource endpoint: GET /notes
Backend pattern: read current state → compute new state → write back → respond.
Test:
  • curl http://localhost:3000/notes[] (for now)

Step 7: POST /notes (Create a note)

COPY-PASTE: src/server.mjs (v5)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v4:
  • Re-introduced readBody() (we need it for POST)
  • Added nextId(notes) (max+1) so ids survive server restarts
  • Added POST /notes with validation (text required)
Test:
  • curl -X POST http://localhost:3000/notes -H "Content-Type: application/json" -d '{"text":"first note"}'
  • Then: curl http://localhost:3000/notes

Step 8: GET /notes/:id (Read one note)

COPY-PASTE: src/server.mjs (v6)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v5:
  • Added parseId() to extract ids from paths
  • Added GET /notes/:id and implemented “not found” logic
Route params are core backend skill: almost every REST API uses them.
Test:
  • curl http://localhost:3000/notes/1
  • curl http://localhost:3000/notes/999 → 404

Step 9: PUT /notes/:id (Update)

COPY-PASTE: src/server.mjs (v7)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v6:
  • Added PUT /notes/:id
  • Validated text again (never trust client input)
  • Persisted back to notes.json
Test:
  • curl -X PUT http://localhost:3000/notes/1 -H "Content-Type: application/json" -d '{"text":"updated note"}'
  • curl http://localhost:3000/notes/1

Step 10: DELETE /notes/:id (Delete)

COPY-PASTE: src/server.mjs (v8)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v7:
  • Added DELETE /notes/:id
  • On success: no JSON body, status 204
Test:
  • curl -i -X DELETE http://localhost:3000/notes/1 → status 204
  • curl http://localhost:3000/notes

Step 12: Add CORS (only if you plan a browser frontend)

COPY-PASTE: src/server.mjs (v9 - with CORS)
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
What changed from v8:
  • Added setCors(res) headers
  • Handled OPTIONS preflight requests
  • Made PORT configurable (env var)
In production, never allow every origin. Restrict to the frontend domain(s).

Quick test suite (copy these curl commands)

COPY-PASTE: terminal commands
📄 terminal.txt
Ln 1, Col 1Text
When a backend “works”, it means:
  • Correct status codes
  • Correct JSON response shapes
  • Data actually persists after restart

Practice problems

Possible backend extensions:
  • Search: GET /notes?contains=word filters notes by substring (case-insensitive)
  • Limit: GET /notes?limit=5 returns only the first N notes
  • Sort: GET /notes?order=desc returns newest notes first
  • Count: GET /notes/count returns {"count": number}
  • Validation: invalid limit values return 400 Bad Request
  • Timestamp filter: GET /notes?updatedAfter=TIMESTAMP returns recently updated notes only
Useful query-parameter pattern:
  • const url = new URL(req.url, 'http://localhost');
  • url.searchParams.get('contains')
  • url.searchParams.get('limit')

Full final code (one last time)

FINAL: src/server.mjs
📄 src/server.mjs
Ln 1, Col 1JavaScript (Node)
FINAL: data/notes.json
📄 data/notes.json
Ln 1, Col 1JSON