typicode/json-server · warning
Not Found
Error message
Not Found
What it means
This 404 is produced by the terminal middleware at src/app.ts:159-167 that every /:name route falls through to. Each handler stores its result in res.locals['data']; when that value is undefined the middleware answers {"error":"Not Found"}. undefined is the Service layer's signal that the named resource or the record with that id does not exist in the lowdb JSON file, so one response covers both an unknown collection and an unknown id.
Source
Thrown at src/app.ts:162
app.put('/:name', withBody(service.update.bind(service)))
app.put('/:name/:id', withIdAndBody(service.updateById.bind(service)))
app.patch('/:name', withBody(service.patch.bind(service)))
app.patch('/:name/:id', withIdAndBody(service.patchById.bind(service)))
app.delete('/:name/:id', async (req, res, next) => {
const { name = '', id = '' } = req.params
res.locals['data'] = await service.destroyById(name, id, req.query['_dependent'])
next?.()
})
app.use('/:name', (req, res) => {
const { data } = res.locals
if (data === undefined) {
res.status(404).json({ error: 'Not Found' })
} else {
if (req.method === 'POST') res.status(201)
res.json(data)
}
})
return app
}
View on GitHub (pinned to 89a34a44b7)
Solutions
- Check that the resource name matches a top-level key in the db JSON file exactly, including pluralization and case
- List the collection first (GET /:name) and confirm the id exists before calling /:name/:id
- If the db file was reset or moved, re-seed it or point the app at the correct JSON file
- Treat 404 in the client as a normal 'resource absent' signal instead of retrying the same call
Example fix
// before
const res = await fetch('/posts/999')
if (!res.ok) throw new Error(String(res.status))
// after
const res = await fetch('/posts/999')
if (res.status === 404) {
// record absent: create it or report to the user
} Defensive patterns
Strategy: type-guard
Validate before calling
// Probe for existence before acting on a specific id
const res = await fetch(`/posts/${encodeURIComponent(id)}`)
if (res.status === 404) {
// create the record or skip, instead of issuing the write
} Type guard
async function resourceExists(name: string, id: string): Promise<boolean> {
const res = await fetch(`/${name}/${encodeURIComponent(id)}`)
return res.ok
}
// usage: (await resourceExists('posts', id)) && await updateById(id, patch) Try / catch
// This is an HTTP 404 response, not a thrown exception
try {
const res = await fetch(`/posts/${id}`)
if (!res.ok && res.status !== 404) throw new Error(`unexpected ${res.status}`)
if (res.status === 404) {
// absent collection or id: seed, recreate, or surface 'not found'
}
} catch (e) {
// network-level failure only; 404 is handled above
} Prevention
- Derive resource names from one shared constant so client and db keys cannot diverge
- Seed the db JSON file in setup scripts and assert expected keys in test fixtures
- Treat 404 as an expected outcome for GET/DELETE-by-id flows, not a crash
- Confirm the db file path the server uses matches the one you inspected by hand
When it happens
Trigger: GET /users when the db JSON has no "users" top-level key; GET /posts/999 or DELETE /posts/999 for an id that does not exist; a singular/plural mismatch such as GET /post instead of /posts; a case mismatch such as /Posts, since the resource name is matched exactly against the db key.
Common situations: Client and server disagree on the resource name (typo, singular vs plural, casing); the lowdb JSON file was reset, moved, or pointed at the wrong path so previously known ids vanish; the record was deleted by another client between read and write; tests run against a fresh empty db expecting seeded fixtures.
AI-assisted analysis of typicode/json-server@89a34a44b7 (2026-08-25).
Data as JSON: /api/errors/ab48a00e0b363b72.
Report an issue: GitHub.