NoNode.js · Lesson 6 of 7

Mini Project: REST API with Persistence

Let's build a simple REST API for a notes app that saves data to a JSON file. No database setup required — just Node, Express, and the file system.

JavaScript
// notes-api/src/index.js
// npm install express
// Run: node src/index.js

import express from 'express'
import { readFile, writeFile } from 'fs/promises'
import { existsSync } from 'fs'
import { randomUUID } from 'crypto'

const app   = express()
const DB    = "./notes.json"

app.use(express.json())

// Load / save helpers
async function load() {
  if (!existsSync(DB)) return []
  return JSON.parse(await readFile(DB, "utf-8"))
}

async function save(notes) {
  await writeFile(DB, JSON.stringify(notes, null, 2))
}

// GET /notes
app.get("/notes", async (req, res) => {
  const notes = await load()
  const { q } = req.query
  res.json(q ? notes.filter(n => n.body.includes(q) || n.title.includes(q)) : notes)
})

// GET /notes/:id
app.get("/notes/:id", async (req, res) => {
  const notes = await load()
  const note  = notes.find(n => n.id === req.params.id)
  if (!note) return res.status(404).json({ error: "Not found" })
  res.json(note)
})

// POST /notes
app.post("/notes", async (req, res) => {
  const { title, body } = req.body
  if (!title || !body) return res.status(400).json({ error: "title and body required" })

  const notes = await load()
  const note  = { id: randomUUID(), title, body, createdAt: new Date().toISOString() }
  notes.push(note)
  await save(notes)
  res.status(201).json(note)
})

// PATCH /notes/:id
app.patch("/notes/:id", async (req, res) => {
  const notes = await load()
  const idx   = notes.findIndex(n => n.id === req.params.id)
  if (idx === -1) return res.status(404).json({ error: "Not found" })

  notes[idx] = { ...notes[idx], ...req.body, updatedAt: new Date().toISOString() }
  await save(notes)
  res.json(notes[idx])
})

// DELETE /notes/:id
app.delete("/notes/:id", async (req, res) => {
  const notes    = await load()
  const filtered = notes.filter(n => n.id !== req.params.id)
  if (filtered.length === notes.length) return res.status(404).json({ error: "Not found" })
  await save(filtered)
  res.status(204).end()
})

app.listen(3000, () => console.log("Notes API: http://localhost:3000"))
Bash
# Test with curl
curl -X POST http://localhost:3000/notes \
  -H "Content-Type: application/json" \
  -d '{"title":"My First Note","body":"Hello, Node!"}'

curl http://localhost:3000/notes
curl http://localhost:3000/notes?q=Hello

# Replace <id> with an actual ID from the POST response
curl -X PATCH http://localhost:3000/notes/<id> \
  -H "Content-Type: application/json" \
  -d '{"body":"Updated body"}'

curl -X DELETE http://localhost:3000/notes/<id>
◆ Note
Next steps: replace the JSON file with SQLite using the `better-sqlite3` package, add request validation with `zod`, or add authentication with `jsonwebtoken`. The structure (load/save helpers, CRUD routes) scales to a real database without major changes.