When you're building a frontend, you often need an API before the real one exists. A mock API fills that gap: you describe some data in a JSON file, and the server hands it back over real REST routes — list, fetch, create, update, delete — so your app has something honest to talk to. This build is that server in one small Express file, and it's driven entirely by a data.json you control.
| At a glance | |
|---|---|
| You need | A server running the Node.js application |
| Plan | Any plan — free runs while your session timer has time left, premium runs 24/7 |
| Time | About fifteen minutes |
| No server yet? | Create your first app server |
If Node is new to you, skim Node.js on Falix first — it covers the index.js entry file, automatic npm install, and the SERVER_PORT rule used below.
Step 1 — Describe your data
Create data.json in the server's root folder. Every top-level key becomes a collection with its own REST endpoints. Each item just needs an id:
{
"users": [
{ "id": 1, "name": "Ada Lovelace", "role": "admin" },
{ "id": 2, "name": "Grace Hopper", "role": "member" }
],
"posts": [
{ "id": 1, "userId": 1, "title": "Hello world", "body": "First post." }
]
}
Add collections and fields freely — the server reads whatever is here. No code changes needed to add a products or comments endpoint; just add the key.
Step 2 — The server
package.json:
{
"name": "json-api-mock",
"version": "1.0.0",
"private": true,
"main": "index.js",
"dependencies": {
"express": "^4.21.2"
}
}
index.js:
const express = require('express');
const fs = require('fs');
const path = require('path');
const DB_FILE = path.join(__dirname, 'data.json');
const load = () => JSON.parse(fs.readFileSync(DB_FILE, 'utf8'));
const save = (data) => fs.writeFileSync(DB_FILE, JSON.stringify(data, null, 2));
const app = express();
app.use(express.json());
// Let any frontend running on another origin call this mock.
app.use((req, res, next) => {
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Methods', 'GET,POST,PUT,DELETE,OPTIONS');
res.header('Access-Control-Allow-Headers', 'Content-Type');
if (req.method === 'OPTIONS') return res.sendStatus(204);
next();
});
// Root lists the collections defined in data.json.
app.get('/', (req, res) => {
res.json({ collections: Object.keys(load()) });
});
app.get('/:collection', (req, res) => {
const items = load()[req.params.collection];
if (!items) return res.status(404).json({ error: 'No such collection' });
res.json(items);
});
app.get('/:collection/:id', (req, res) => {
const items = load()[req.params.collection];
if (!items) return res.status(404).json({ error: 'No such collection' });
const item = items.find((x) => String(x.id) === req.params.id);
if (!item) return res.status(404).json({ error: 'Not found' });
res.json(item);
});
app.post('/:collection', (req, res) => {
const data = load();
const items = data[req.params.collection];
if (!items) return res.status(404).json({ error: 'No such collection' });
const nextId = items.reduce((m, x) => Math.max(m, Number(x.id) || 0), 0) + 1;
const item = { id: nextId, ...req.body };
items.push(item);
save(data);
res.status(201).json(item);
});
app.put('/:collection/:id', (req, res) => {
const data = load();
const items = data[req.params.collection];
if (!items) return res.status(404).json({ error: 'No such collection' });
const i = items.findIndex((x) => String(x.id) === req.params.id);
if (i === -1) return res.status(404).json({ error: 'Not found' });
items[i] = { ...items[i], ...req.body, id: items[i].id };
save(data);
res.json(items[i]);
});
app.delete('/:collection/:id', (req, res) => {
const data = load();
const items = data[req.params.collection];
if (!items) return res.status(404).json({ error: 'No such collection' });
const i = items.findIndex((x) => String(x.id) === req.params.id);
if (i === -1) return res.status(404).json({ error: 'Not found' });
const [removed] = items.splice(i, 1);
save(data);
res.json(removed);
});
const PORT = process.env.SERVER_PORT || 8080;
app.listen(PORT, '0.0.0.0', () => console.log(`Listening on port ${PORT}`));
What each route does
The :collection in a route is a wildcard — one set of handlers serves every key in data.json. Point them at /users, /posts, or anything you add.
| Route | Method | Result |
|---|---|---|
/ |
GET | The list of collection names |
/:collection |
GET | Every item in that collection |
/:collection/:id |
GET | One item, or 404 |
/:collection |
POST | Creates an item, assigns the next id, returns 201 |
/:collection/:id |
PUT | Merges your fields into the item |
/:collection/:id |
DELETE | Removes the item, returns it |
🎯 Good to know: The CORS block at the top is what lets a frontend on a different address call this mock without the browser blocking it. That's the whole point of a mock server — it usually runs somewhere other than the page hitting it.
Step 3 — Start it and hit the routes
Press Start. Once the console shows Listening on port …, open your server's address (from the Network page). Visit / to see your collections, /users for the list, /users/1 for one record. From your frontend, fetch('http://your-address:port/users') returns the array.
Creating and editing works from any HTTP client:
curl -X POST -H "Content-Type: application/json" \
-d '{"name":"Linus","role":"member"}' \
http://YOUR_ADDRESS:PORT/users
You'll get back the new record with an id filled in.
Persistence and its honest limits
Writes are saved straight back to data.json, so a POST survives a restart — this is a stateful mock, not just a static fixture.
⚠️ Heads up: Two things to keep in mind. First, this is a single JSON file with no locking — great for one developer testing a frontend, not for real production traffic. Second,
data.jsonis a server file: a reinstall or application switch wipes it. Keep a copy of your seed data in Git, or back it up, so you can restore the starting state. When you need a real datastore, graduate to the todo API build or a managed database.
To reset the mock to its original data, stop the server, paste your seed JSON back into data.json, and start again.
Troubleshooting
- Frontend gets a CORS error — the browser is blocking the call. Confirm the CORS middleware is present and sits above your routes; it must run on every request, including the
OPTIONSpreflight. 404 No such collection— the URL's first segment must match a key indata.jsonexactly (/users, not/user). CheckGET /for the real names.- Edits don't stick — make sure the request sets
Content-Type: application/jsonsoexpress.json()parses the body; without it,req.bodyis empty. - Can't reach it from the browser — a port or bind issue: I can't reach my app.
Beyond this, it's ordinary Express — expressjs.com documents routing, middleware, and everything you'd add to grow the mock into a real API.