Build a mock API server

A configurable fake REST API driven by a single JSON file — full GET/POST/PUT/DELETE on any collection you define, with CORS on, so you can build and test a frontend before the real backend exists.

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.json is 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 OPTIONS preflight.
  • 404 No such collection — the URL's first segment must match a key in data.json exactly (/users, not /user). Check GET / for the real names.
  • Edits don't stick — make sure the request sets Content-Type: application/json so express.json() parses the body; without it, req.body is 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.


Next steps

Was this guide helpful?