A dashboard for frontend devs to stand up fake HTTP endpoints — define a path, paste the JSON you expect back, and call it from your app. No backend team, no waiting for a staging deploy.
- Mock endpoints — method + path + status + JSON body, editable from the dashboard.
- Global response headers — sent on every mock response; endpoints can override them.
- Simulated delay — a global delay, overridable per endpoint, for exercising loading states.
- Request logs — a live view of incoming traffic with the status that went back.
- Dynamic responses — echo request values with defaults, and generate fake data with Faker.js.
Requires Node 20.9+ and git. One command clones, installs, starts, and opens the dashboard:
curl -fsSL https://raw.githubusercontent.com/thesanjeevsharma/mock8/main/scripts/install.sh | bashIt clones into ./mock8, picks the next free port if 6767 is taken, and leaves
the dev server in the foreground — Ctrl+C stops it. Re-run it any time to pull
the latest and start again; it skips the reinstall when nothing changed, and
won't touch a checkout with uncommitted work.
Pass options after -s --:
curl -fsSL <url>/scripts/install.sh | bash -s -- --dir ~/tools/mock8 --port 7000| Flag | Env var | Default |
|---|---|---|
--dir PATH |
MOCK8_DIR |
./mock8 |
--port N |
PORT |
6767 |
--branch NAME |
MOCK8_BRANCH |
main |
--no-open |
MOCK8_OPEN=0 |
opens |
--no-start |
MOCK8_START=0 |
starts |
Piping a script into bash runs whatever the URL serves. If you'd rather read it
first — please do — it's scripts/install.sh, or just do
it by hand:
git clone https://github.com/thesanjeevsharma/mock8.git
cd mock8
npm install
npm run devEither way, open http://localhost:6767. It redirects to the dashboard and seeds two examples — a plain JSON response and one using request values plus fake data — so there's something working immediately:
curl -i http://localhost:6767/users/me
curl -i -X POST http://localhost:6767/api/v2/signup \
-H 'Content-Type: application/json' -d '{"email":"you@example.com"}'No database to provision, no config file to fill in.
| Command | What it does |
|---|---|
npm run dev |
dev server with hot reload on port 6767 |
npm run build |
production build |
npm start |
serve the production build (also 6767) |
npm run lint |
ESLint |
Both are optional.
| Variable | Default | Effect |
|---|---|---|
PORT |
6767 |
port for dev and start |
MOCK8_LOG_RETENTION_MS |
900000 |
how long request logs are kept (15 min) |
PORT=4000 npm run dev
MOCK8_LOG_RETENTION_MS=60000 npm run dev # keep 1 minute of logsMocks are served straight from the root, so point your frontend's base URL at the origin — no prefix:
http://localhost:6767
A mock defined as GET /api/v1/users answers at
GET http://localhost:6767/api/v1/users.
// in your app
const api = axios.create({ baseURL: "http://localhost:6767" });
await api.get("/api/v1/users");The dashboard gets out of the way under /mock8, which is the only path
space a mock can't claim (http://localhost:6767 redirects there so the bare URL
still opens something useful). Trying to mock a reserved path is rejected in the
dialog rather than silently ignored:
| Reserved | Why |
|---|---|
/ |
redirects to the dashboard |
/mock8/* |
the dashboard and its own API |
/_next/* |
Next.js internals |
Everything else is yours, /api/* included.
CORS is permissive by default (the caller's origin is reflected, credentials are allowed, preflights are answered), so a frontend on any other port can call these from the browser. Turn it off in Settings if you want to test CORS failures.
| Pattern | Matches |
|---|---|
/users/me |
exactly /users/me |
/users/:id |
/users/42, /users/abc |
/api/v:version/me |
/api/v2/me |
/files/* |
/files/a, /files/a/b/c |
A :param can take a whole segment or part of one. Query strings are ignored
when matching. The most specific match wins — the more literal text a pattern
pins down, the higher it ranks, so /orders/latest beats /orders/:id and
/api/v:version beats /api/:anything. Matched params come back on the
X-Mock8-Path-Params response header.
Response bodies are templates. Each helper takes a path or name, plus an optional default as a second argument:
| Helper | Reads from |
|---|---|
{{body 'name.first' 'Anon'}} |
JSON request body — dot paths and items.0.id |
{{pathParam 'id'}} |
a :param captured by the endpoint's path |
{{queryParam 'page' '1'}} |
query string |
{{header 'authorization'}} |
request header (case-insensitive) |
{{cookie 'session' 'none'}} |
Cookie header |
{{faker 'string.uuid'}} |
generated fake data — see below |
Quoting decides the type. Inside quotes you get a string; unquoted you get the real JSON value:
String values are escaped for the position they land in, so a value containing
quotes or newlines can't break your JSON. Use the triple-brace form
{{{body 'fragment'}}} to insert a value unescaped when you want it verbatim.
{{faker 'namespace.method'}} calls Faker.js, so
essentially any namespace works. Arguments come in the documented three forms:
{
"id": "{{faker 'string.uuid'}}",
"country": "{{faker 'location.country'}}",
"small": {{faker 'number.int' '100000'}},
"ranged": {{faker 'number.int' '{min:500, max:600}'}},
"price": {{faker 'number.float' '{fractionDigits:2}'}},
"when": "{{faker 'date.future' 'iso'}}",
"born": "{{faker 'date.past' 'us'}}"
}For date.* methods a string second argument is a format and an object
is passed to faker. Formats: iso, iso8601, rfc2822, unix, timestamp,
short, long, full, compact, utc, sql, us, eu, or your own tokens
like YYYY-MM-DDTHH:mm:ss.SSSZ. unix and timestamp come out as numbers.
A missing value with no default becomes null (empty inside a string) and is
listed on the X-Mock8-Unresolved response header and in the Logs tab — a typo
shows up instead of silently serving nulls.
A broken template is different: bad syntax is rejected when you save, and a
bad faker attribute returns 561 with the reason in the body, so you never
mistake it for your mock's own status. Bodies without {{ are served
byte-for-byte as authored.
Headers are applied in this order, last one winning:
Content-Type: application/json- CORS headers (if enabled)
- Global headers (Settings)
- The endpoint's own headers
Delay resolves as endpoint delay ?? global delay, capped at 60s. Every response
carries X-Mock8: hit | miss | preflight so you can tell a mock apart
from a real backend in devtools.
204, 205, 304 and HEAD requests are sent without a body per spec. A
HEAD request with no HEAD mock falls back to the matching GET mock.
The Logs tab is a live tail of every request that reaches a mock path. The dashboard's own API calls are not logged, so the tab can't fill with its own polling. Each row is recorded once the response is settled, so the status code is the real one that went back:
| Column | Meaning |
|---|---|
| Time | local wall-clock, to the millisecond |
| Path | as requested, query string included |
| Status | the status actually sent |
| Outcome | hit · miss 404 · preflight CORS · template-error 561 |
| Took | total time the caller waited, simulated delay included |
Expanding a row shows the pattern that matched, extracted path params, how much of the duration was simulated delay, the calling origin, and the request body (truncated at 1 KB).
Polls every 2s; Live pauses without losing anything, since it resumes from a cursor. Clear empties the buffer immediately.
Logs are in memory, capped at 1000 entries, and entries older than
15 minutes are dropped — both on read and by a sweeper timer. Nothing is
written to disk, so restarting the dev server starts a fresh tail. Adjust the
window with MOCK8_LOG_RETENTION_MS.
Endpoints and settings live in data/db.json, created on first run and
gitignored. Reads hit the file on every request with no caching, so a hand-edit
takes effect immediately — no restart. Writes are queued and atomic, so a crash
mid-save can't leave a truncated file.
It's a file, not a database: fine for one dev on localhost, but it won't hold up
as a shared service with concurrent writers. Everything storage-related is behind
the exported functions in src/lib/db.ts, so swapping it out wouldn't touch the
rest of the app.
The dashboard drives a small JSON API you can script against. It lives under
/mock8/api so that a plain /api/* stays yours to mock:
| Route | Purpose |
|---|---|
GET /mock8/api/endpoints |
list endpoints + settings |
POST /mock8/api/endpoints |
create an endpoint |
PATCH /mock8/api/endpoints/:id |
partial update |
DELETE /mock8/api/endpoints/:id |
delete |
GET /mock8/api/settings |
read global headers / delay / CORS |
PUT /mock8/api/settings |
update settings |
GET /mock8/api/logs?since=N |
request logs after cursor N |
DELETE /mock8/api/logs |
clear the log buffer |
Bodies are validated: JSON must parse, status must be 100–599, the path must not
be reserved, and method + path must be unique.
curl -X POST http://localhost:6767/mock8/api/endpoints \
-H 'Content-Type: application/json' \
-d '{"method":"GET","path":"/api/health","body":"{\"ok\":true}","delayMs":250}'Everything lives on three tabs:
- Endpoints — create, edit, duplicate and delete mocks. The switch in the
Oncolumn disables a mock without deleting it, which is the quick way to make a route 404 and test your error path. The⋯menu copies a ready-to-paste curl command. - Logs — watch traffic as it arrives. Start here when a call isn't behaving:
a
missrow means no mock matched the method and path you think it did. - Settings — global headers, global delay, CORS.
A few things worth knowing:
- Filtering — both tables filter as you type; Logs also filters by outcome, so you can show only misses.
- Sharing a set of mocks —
data/db.jsonis a plain file you can send to a teammate or hand-edit; changes are picked up on the next request, no restart. It's gitignored by default, so drop the/dataline from.gitignoreif you'd rather commit a shared set. - Starting over — delete
data/db.jsonand it re-seeds on the next request.
src/
app/
[...path]/route.ts the mock server — every request that isn't the dashboard
page.tsx / redirects to the dashboard
mock8/ the dashboard: pages + its own API under /api
components/ UI — endpoints-view, logs-view, settings-form are the tabs
lib/
db.ts JSON-file store, validation, atomic writes
paths.ts path normalizing + pattern matching and specificity
template.ts template rendering: request helpers + faker (server only)
template-syntax.ts template parsing, JSON-aware validate + format (shared)
logs.ts in-memory request log ring buffer
types.ts shared types, DASHBOARD_PREFIX, reserved paths
scripts/
install.sh the one-line installer
Where to make common changes:
| To change… | Edit |
|---|---|
| the dashboard's URL prefix | DASHBOARD_PREFIX in src/lib/types.ts (and rename the folder) |
| path-matching or specificity | compileSegment / matchScore in src/lib/paths.ts |
| template helpers or faker args | resolve / callFaker in src/lib/template.ts |
| template parsing or validation | src/lib/template-syntax.ts |
| validation rules for saving | validate in src/lib/db.ts |
| log retention or buffer size | the constants at the top of src/lib/logs.ts |
The dashboard folder must not start with an underscore — Next treats
_folder as private and silently drops it from routing.
@faker-js/faker must stay out of the client bundle — that's why template
parsing lives in template-syntax.ts (imported by the dialog) and rendering in
template.ts (server only). Keep faker imports on the server side of that line.
Before pushing: npm run lint && npx tsc --noEmit && npm run build. There's no
test suite; paths.ts, template.ts and template-syntax.ts are pure functions
and would be the natural place to start one.
A call 404s that shouldn't. Check the Logs tab — the miss row shows the
exact method and normalized path that was looked up. Usual causes are a method
mismatch, a disabled endpoint, or a :param that doesn't line up.
"Another next dev server is already running." Next 16 allows one dev server
per directory. Stop the other one: lsof -ti:6767 | xargs kill.
Port already in use. Run on another port with PORT=4000 npm run dev.
Logs are empty after a restart. Expected — they're in memory by design, so a
restart starts a fresh tail. Endpoints and settings persist in data/db.json.
CORS errors in the browser. Confirm CORS is on in Settings, and that you're calling the mock origin directly rather than through a proxy that strips headers.
Next.js (App Router) · TypeScript · Tailwind CSS v4 · shadcn/ui
{ "who": "{{body 'name.first'}}", // "Ada" "age": {{body 'age' '0'}}, // 20 — a number, or 0 if absent "active": {{body 'active'}}, // true "user": {{body 'name'}}, // {"first":"Ada","last":"Lovelace"} "hello": "Hi {{body 'name.first'}}, page {{queryParam 'page' '1'}}" }