CRUD Operations
CRUD — Create, Read, Update, Delete — is the backbone of almost every API. With the endpoint design settled, this page implements the full cycle for one resource, paying attention to the details that separate a toy example from a correct API: the right status codes, PUT vs PATCH semantics, returning the right body, and handling "not found" consistently.
The complete CRUD-to-HTTP map
Operation | Method + path | Success status | Body returned |
|---|---|---|---|
Create |
|
| The new resource |
Read (list) |
|
| Array of resources |
Read (one) |
|
| The resource |
Replace |
|
| The updated resource |
Update |
|
| The updated resource |
Delete |
|
| (empty) |
Setup
tasks.js — a CRUD router
import { Router } from 'express'
const router = Router()
// Stand-in data store (swap for a database later):
let tasks = [{ id: 1, title: 'Learn REST', done: false }]
let nextId = 2
const findTask = (id) => tasks.find((t) => t.id === Number(id))
export default routerCreate — POST
router.post('/', (req, res) => {
const { title } = req.body
if (typeof title !== 'string' || !title.trim()) {
return res.status(422).json({ error: 'title is required' })
}
const task = { id: nextId++, title: title.trim(), done: false }
tasks.push(task)
res
.status(201) // 201 Created
.location(`/tasks/${task.id}`) // where to find it
.json(task) // echo the created resource
})Read — GET (list and one)
// List — return an array (or a paginated envelope):
router.get('/', (req, res) => {
res.json(tasks)
})
// One — 404 if it doesn't exist:
router.get('/:id', (req, res) => {
const task = findTask(req.params.id)
if (!task) return res.status(404).json({ error: 'Task not found' })
res.json(task)
})PUT vs PATCH — the crucial distinction
// PUT = full replacement. Missing fields are RESET, not preserved.
router.put('/:id', (req, res) => {
const task = findTask(req.params.id)
if (!task) return res.status(404).json({ error: 'Task not found' })
const { title, done } = req.body
if (typeof title !== 'string') {
return res.status(422).json({ error: 'title is required for PUT' })
}
task.title = title
task.done = Boolean(done) // omitted → reset to false (full replace)
res.json(task)
})
// PATCH = partial update. Only provided fields change.
router.patch('/:id', (req, res) => {
const task = findTask(req.params.id)
if (!task) return res.status(404).json({ error: 'Task not found' })
if (req.body.title !== undefined) task.title = req.body.title
if (req.body.done !== undefined) task.done = Boolean(req.body.done)
res.json(task)
})Delete — DELETE
router.delete('/:id', (req, res) => {
const before = tasks.length
tasks = tasks.filter((t) => t.id !== Number(req.params.id))
if (tasks.length === before) {
return res.status(404).json({ error: 'Task not found' })
}
res.status(204).end() // 204 No Content — empty body
})Async with a real database
// Same shapes, now awaiting the data layer (Express 5 auto-catches rejections):
router.get('/:id', async (req, res) => {
const task = await db.tasks.findById(req.params.id)
if (!task) return res.status(404).json({ error: 'Task not found' })
res.json(task)
})
router.post('/', async (req, res) => {
const task = await db.tasks.create({ title: req.body.title })
res.status(201).location(`/tasks/${task.id}`).json(task)
})CRUD correctness checklist
Create →
201+Location+ the new resource.List →
200+ array ([]when empty, never404).Fetch/update/delete a missing id →
404, consistently.PUTfully replaces;PATCHmerges — implement what the verb promises.Validate
req.bodybefore writing; respond422on bad input.Delete →
204with no body (or200+ the object — pick one).