http-mock-json
v6.0.1
Published
Mock your real API in JSON — status codes, errors, validation, latency, and mutable data — so the frontend can develop and test without waiting on a backend.
Maintainers
Readme
Mock your real API in JSON — status codes, errors, validation, latency, and mutable data — so the frontend can develop and test without waiting on a backend.
Define the same endpoints your app will call. Switch success and failure scenarios, validate request shapes, persist collections, proxy selected routes to a live server — or record staging once and replay from .recordings/ with no backend online.
The frontend keeps calling HTTP on your machine. Backend outages stop blocking you. Use flat mocks/*.json or microservice folders; proxy only the routes that should still hit a live service, or capture them with Record & Replay and keep working offline.
Why http-mock-json
- JSON-first — describe routes and responses in plain files; no code required to mock an API
- Frontend-ready — status codes, headers, delays, multipart/raw bodies, and mutable CRUD stores
- Safe by default — startup validation catches broken mocks before they waste your time
- Watch mode — server restarts when mock files change
- Opt-in depth — start static; add
match,request,store, orproxyonly when you need them - Presets —
mock-server add --preset …scaffolds common shapes (CRUD, auth, upload, relations…) so you edit JSON instead of inventing structure - Record & Replay — point
--proxyat staging, hit the app once with--record, then replay those fixtures locally without the upstream
Quick Start
Requires Node.js >= 22.12. This package is a CLI (mock-server) that serves JSON mock files over HTTP (not an embeddable SDK).
npm install http-mock-json --save-dev
npx mock-server init
npx mock-server start
curl -i http://localhost:3001/<your-endpoint>Default port is 3001 (npm run mock:start after init uses the same). Full walkthrough: Getting started.
Documentation
This README is the full guide and reference. Quick help: FAQ · Troubleshooting · Changelog.
Day-one path: Getting started → optional Concepts. Everything else is lookup when you need it.
For AI assistants
Start with the pocket brief docs/ai.md (pipeline, presets, CLI, limits). Index of priority docs: llms.txt. These ship on npm under node_modules/http-mock-json/ — open or @-mention them; IDEs do not auto-inject them into every chat.
Contents
Learn (day one):
- Getting started
- Concepts (optional — short glossary + how pieces fit)
Later (when you need it):
- Examples
- Advanced examples
- Record & Replay — capture staging, replay offline
Reference (lookup when needed):
- CLI reference
- Mock file reference
- Validation reference
- Body compatibility
- Mock config reference
- Store reference
Recipes (product-style; after you know the basics):
- Store recipes — store-backed app walkthroughs (Examples C–R)
- Real-world — multi-feature / multipart / folders / hybrid proxy
Getting started
Goal
In about five minutes: install, create a first mock, start the server, curl a response, then switch scenarios by editing JSON.
Prerequisites
- Node.js 22.12 or newer
- A Node project with a
package.json(recommended soinitcan add a start script)
Steps
1. Install
npm install http-mock-json --save-dev2. Initialize
npx mock-server initBy default this will:
- Create a
mocksdirectory - Add a
mock:startscript (mock-server start) - Prompt you to create a first mock file
3. Answer the prompts
init uses the static preset and asks for a file name, endpoint (e.g. data/animals), HTTP methods, then confirm. The scaffold already includes sample bodies so you can curl immediately:
{
"data/animals": {
"GET": {
"nameResponse": "success",
"responses": [
{
"name": "success",
"statusCode": 200,
"body": { "message": "ok" }
},
{
"name": "error",
"statusCode": 404,
"body": { "message": "Not found" }
}
]
}
}
}Edit those bodies to match your API, or flip "nameResponse" to "error" to try the 404 path. More fields (match, store, proxy, …) come later in Concepts.
4. Start the server
npx mock-server startOr npm run mock:start. Both listen on http://localhost:3001 by default. Watch mode reloads when you save mock files; validation errors block startup (and a bad reload).
5. Try it
curl -i http://localhost:3001/data/animalsYou should see { "message": "ok" }. Change "nameResponse" to "error", save, and curl again — same URL, "Not found" (watch mode reloads).
Expected result
- A mocks directory with at least one
.jsonfile - Server at
http://localhost:3001 curlreturns thenameResponsebody- Editing
nameResponse(or bodies) changes the next response after reload
Next steps
- Concepts — how
nameResponse,match,request,store, andproxyfit together
When you need scaffolds or copy-paste fixtures: CLI — add (--preset …), Examples.
Other ways to start
Prefer capturing a real API instead of hand-writing mocks? Use Record & Replay (--proxy + --record). OpenAPI → JSON: CLI — import.
Concepts
Goal
Understand the main ideas behind an http-mock-json mock — enough to choose the right feature — without memorizing every field.
Prerequisites
- You can start the server from Getting started
- You have at least one mock JSON file under your mocks directory
Core ideas
Named responses and nameResponse
Each HTTP method on an endpoint holds a responses array. Every entry has a unique name.
nameResponse is the fallback: when no response’s match applies, the server returns the response whose name equals nameResponse.
Use this to flip success vs error (or any scenario) by editing one string. Field rules: mock file reference.
match — pick a scenario from the request
Optional match on a response selects that scenario when the incoming request fits. Rules can use:
params— route params (/users/:id)query— query stringbody— parsed JSON or urlencoded fieldsheaders— header names (case-insensitive)multipart— multipart fields / file metadatacall— N-th hit (1-based), with optional scope,loop, andreset
Matching is partial: only the keys you list must agree. First matching response in array order wins; if none match, nameResponse is used.
Empty match objects do not match. Details and examples: mock file, samples in Examples.
request — validate before selecting a happy path
Optional method-level request checks the inbound payload, query, and/or headers before normal match / nameResponse selection.
Important naming (v4+):
- Use
request.payload(notrequest.body) - Grouped error options live under
request.error(response,format,detail,key)
You can set as (json | form | multipart | raw | text) to require a content mode, or omit it and let Content-Type decide. Failed checks select the configured error response instead of running the usual match pipeline.
Depth: body compatibility, validation, mock file.
store — mutable collections
Opt-in per endpoint: declare a collection with store (or reference a shared store.id defined elsewhere). Responses can use action (list, get, create, update, patch, delete, restore) instead of a fixed body.
Typical capabilities (see Store reference): seed data, persistence, list filters/pagination, soft delete, relations, unique / key conflicts.
action cannot share a response with proxy or response encoding.
proxy — forward to a real backend
Proxy can appear at several layers:
- Response:
"proxy": true, a URL string, or{ "target", "path?" } - Method default target for
"proxy": true - CLI
--proxyand foldermock.config.jsontargets for"proxy": trueand unmatched routes
When a selected response has proxy set, the original request is forwarded and the mock body / action path is skipped. Unmatched routes can still be forwarded if you configured global or folder unmatched proxy mounts. Upstream redirects are not followed (redirect: "manual"); the mock returns the 3xx response as-is.
Record & Replay — capture staging, work offline
Hand-writing every fixture is optional. With --proxy and --record, proxied responses are written under .recordings/ (same mock JSON shape). Stop the server, start again without --record: recordings load by default next to your mocks, so the frontend keeps the same URLs with no upstream.
Typical loop: record against staging → commit or share sanitized fixtures → day-to-day mock-server start (or --recordings-only). Full flags and behavior: CLI — Record & Replay.
Runtime pipeline (matching order)
For a request that hits a registered mock route, roughly:
- Parse the body when needed (including multipart). Parse failures can become a
requesterror response ifrequestis configured; otherwise they return a client error. requestvalidation — if configured and issues are found, serve the request-error response and stop.- Response selection — walk
responsesin order; first entry with a satisfiedmatchwins; otherwise usenameResponse. - Delay — method-level and/or response-level latency.
- Fulfill:
proxy→ forward to the resolved targetaction+ store → run the store operation (conflicts / not-found use named responses when configured)- otherwise → send the mock
body(or encoded file / base64 body)
After registered routes, unmatched traffic may still hit folder proxyUnmatched mounts, then the global --proxy catch-all.
Startup checks (port, mock shape, references) are separate from this per-request path — see validation.
CORS is enabled by default (browser frontends can call the mock server without a separate CORS mock).
Glossary
| Term | Meaning |
|------|---------|
| mocks directory | Folder passed to --path / -f (default mocks). Contains mock JSON files and optional mock.config.json. |
| mock file | A .json file loaded by start (not mock.config.json). Top-level keys are endpoints. |
| fixture | Sample mock under this repo’s mocks/ (for copy/paste). |
| endpoint | Top-level key in a mock file (e.g. "data/animals"). In prose, “route” means the HTTP path, not a JSON field name. |
| named response | An entry in responses[] with a name. Prefer this over “scenario” when talking about the API. |
| nameResponse | Fallback named response when no match applies. |
| match | Optional object on a response that selects it from the request (params / query / body / …). |
| request | Method-level validation (payload / query / headers) before response selection. Distinct from startup validation. |
| store definition / reference | Full store object vs { "id": "…" } reuse. Prefer these over bare “schema”. |
| recording | Fixture under .recordings/ written by --record while proxying; loaded on start unless --exclude-recordings. |
Expected result
You can explain, for a given mock, which response will run: validation error, a matched scenario, the nameResponse fallback, a store action, a proxy forward, or a loaded recording.
Next steps
- Examples — copy-paste samples from this repository
- Advanced examples — one-feature walkthroughs
Lookup in this manual when needed:
| Need | Section |
|------|---------|
| Every mock field | Mock file reference |
| CRUD / persist / list / relations | Store |
| Payload modes, files, response encoding | Body compatibility |
| Folder layouts (mock.config.json) | Mock config |
| What fails at startup vs at runtime | Validation |
| CLI flags | CLI |
CLI reference
Binary: mock-server (package http-mock-json, current version 5.1.0). Requires Node.js ≥ 22.12.
Global options: -h / --help, -v / --version.
Flag
-pdepends on the command: oninit/add/import,-pis--path(mocks directory). Onstart,-pis--port; the mocks directory is-f/--path.
init
Create the folder that will contain the mocks.
mock-server init| Flag | Default | Description |
|------|---------|-------------|
| -p, --path <path> | mocks | Path to the mocks directory to create |
| -m, --mock [value] | true | Create a first mock. Accepts true / 1 as true; any other string is false. Bare --mock is true. |
| -s, --script [value] | true | Add a start script to package.json. Same boolean parsing as --mock. |
Examples:
mock-server init --path api-mocks --mock false --script false
mock-server init --path apps/folder1/mocks --mock false --script falsestart
Start the mock server (loads JSON mocks, validates, listens, then watches for file changes).
mock-server start| Flag | Default | Description |
|------|---------|-------------|
| -p, --port <port> | — | Listen port (integer 1–65535). Overrides mock.config.json port when set; otherwise config port, else 3001. |
| -f, --path <path> | mocks | Path to the mocks directory (JSON files + optional mock.config.json) |
| --proxy <url> | — | Global proxy target (http / https). Used by responses with "proxy": true and by unmatched routes (after folder proxyUnmatched mounts). Upstream redirects are not followed (redirect: "manual"); 3xx is returned as-is. |
| --record | false | Record proxied responses into .recordings/ (JSON + binary via encoding: "file"). Requires a proxy target (CLI --proxy, folder proxy / proxyUnmatched, or response proxy). |
| --exclude-recordings | false | Do not load .recordings/ (mocks only). |
| --recordings-only | false | Load only .recordings/ (ignore regular mock JSON). Incompatible with --exclude-recordings. |
| --reset-store [ids] | — | Delete persisted store snapshots before the initial start. Bare --reset-store clears all. Comma-separated ids clear only those store ids (runtime ids, including namespace:id when storeNamespace is set). Not re-applied on watch reloads. |
Examples:
mock-server start --port 3001 --path api-mocks --proxy https://api.staging.com
mock-server start --path apps/folder1/mocks
mock-server start --proxy https://api.staging.com --record
mock-server start --exclude-recordings
mock-server start --recordings-only
mock-server start --reset-store
mock-server start --reset-store notes,users
mock-server start --reset-store users:sessionRecord & Replay
Bootstrap realistic fixtures from a live API, then develop and test with the upstream offline. Recordings use the same mock JSON shape (including match for params/query/auth/multipart), so you can edit or commit them like any other mock.
# 1) Record (writes .recordings/ while proxying)
mock-server start --proxy https://api.staging.com --record
# 2) Stop with Ctrl+C (prints wrote / skipped / proxy failures)
# 3) Replay (default loads mocks + recordings)
mock-server startWith mock.config.json folders, recordings are grouped by longest matching prefix into <folder>/.recordings/. Without config, files go under mocks/.recordings/.
| Behavior | Detail |
|----------|--------|
| What is recorded | Proxied traffic only (local mocks are not overwritten) |
| JSON | Saved as normal response body (including primitives) |
| Binary | encoding: "file" + bytes under .recordings/files/ |
| Bodies | JSON parsed when possible; text/* / xml / csv as strings; binary as encoding: "file"; unknown utf8 as text, otherwise binary. Proxied responses are recorded (no content-type skip for normal API payloads). |
| Redirects | Not followed (redirect: "manual"); 3xx + Location are recorded |
| Path params | Digits → :id / :id2…; segments like v1 / v2 stay literal |
| Headers | Response headers mapped (including Set-Cookie); hop-by-hop omitted. Request Authorization / Cookie go into match.headers so variants of the same route replay correctly |
| Match | Path params, query, non-empty JSON body, auth/cookie headers, and multipart fields/file metadata (filename / mimeType / size); response without match becomes the default (nameResponse) |
| Collisions | In default load mode, a mock route wins over a recording (warning logged) |
| Startup log | Routes grouped under Mocks and Recordings |
.recordings/ writes are ignored by the file watcher (same idea as .store/). The package .gitignore excludes **/.recordings/ because recordings may store Authorization / Cookie in match.headers — commit them only when intentionally sanitized.
Breaking (≥ 2.0.0): --path / -f is the mocks directory itself (default mocks).
Before 2.0.0, -f apps/folder1 meant apps/folder1/mocks. Use -f apps/folder1/mocks now.
Port resolution: CLI -p / --port → mock.config.json port → 3001.
CORS: enabled by default on the HTTP server (including exposing response headers to the browser).
See also: Validation, Mock config, Store — persist / --reset-store.
add
Scaffold a mock JSON file with an interactive prompt. Every add run uses a preset (default static). You always set the file name and endpoint; only static also asks which HTTP verbs to include.
mock-server add
mock-server add --preset crud
mock-server add --path api-mocks --preset auth-login| Flag | Default | Description |
|------|---------|-------------|
| -p, --path <path> | mocks | Path to the mocks directory |
| --preset <name> | static | Which scaffold to write (table below) |
| --crud | false | Alias for --preset crud |
Presets
| Preset | Use when you need… | Writes |
|--------|--------------------|--------|
| static | A ready-to-curl endpoint you can edit | One route; chosen verbs; sample success / error bodies |
| scenarios | Branching without a store | GET + ?scenario=ok\|missing\|error (match + delay) |
| auth-login | Login validation + happy/sad paths | POST + request + match (200 / 403) + 401 / 400 |
| crud | Mutable collection + item | store actions: list / create / get / update / patch / delete |
| crud-full | CRUD plus real persistence rules | crud + persist, unique, softDelete, item restore |
| paginated-list | Tables / infinite scroll against seed data | Collection store.list (page, filter, search) + POST create |
| relations | Parent/child resources with FK | Two stores: parent + sibling child (embed, expand, onDelete: restrict) |
| upload | Multipart create + file download | POST multipart + item GET (encoding: "base64") |
| proxy-hybrid | Some routes local, one live upstream | Local GET + sibling …/live proxy (jsonplaceholder) |
Endpoint conventions (store presets): api/notes → collection + api/notes/:id. A trailing /:param is kept on item routes (users/:userId). store.id is the last non-param segment. proxy-hybrid uses that same collection base for the sibling …/live route (so api/notes/:id still yields api/notes + api/notes/live). Existing files prompt before overwrite.
relations prompts: you name the parent collection (default api/users). The preset always scaffolds a second child collection next to it:
- Default child segment is
posts→ e.g. parentapi/userswritesapi/users+api/posts(storesusers/posts, FKuserId, reverse embed, delete restrict). - If that default would collide with the parent (same path or same
store.id— e.g. parentapi/posts), the CLI asks for the child collection name (suggestioncomments) and writes that sibling instead (api/posts+api/comments, FK derived from the parent id →postId). - Child names are a single path segment (
comments,articles); parent FK / embed names follow the parentstore.id(soft-singularized); child relation keys and conflict names follow the child segment.
mock-server add --preset scenarios
mock-server add --preset crud-full
mock-server add --preset relations --path apps/folder1/mocksadd / init write into the root of the mocks directory (they do not create mock.config.json folder layouts). import writes to the root when there is no server/--prefix; with a route prefix it writes mock.config.json + one-level folders. For folder organization, see Mock config.
import
Generate mock JSON files from an OpenAPI 3.x document (local file or http(s) URL). Offline generator only — start still loads the written JSON, not the OpenAPI file.
mock-server import --openapi ./openapi.yaml
mock-server import --openapi https://example.com/openapi.json -p mocks
mock-server import --openapi ./openapi.yaml --no-split-tags --out my-api --overwrite
mock-server import --openapi ./openapi.yaml --prefix /api/v1
mock-server import --openapi ./openapi.yaml --no-server-prefix| Flag | Default | Description |
|------|---------|-------------|
| --openapi <source> | (required) | OpenAPI 3.0 / 3.1 file path or URL |
| -p, --path <path> | mocks | Path to the mocks directory |
| --out <name> | from info.title (or openapi) | Base file name when using --no-split-tags |
| --no-split-tags | off (split by tag) | Write a single JSON file instead of one file per OpenAPI tag (untagged.json when a tag is missing) |
| --prefix <path> | from servers[0].url path | Route prefix written into mock.config.json folders.*.prefix (overrides the OpenAPI server path) |
| --no-server-prefix | off | Ignore servers[0] path; write flat mock JSON at the mocks root (no mock.config.json) |
| --overwrite | false | Overwrite existing files without prompting |
| --no-request | off (generate request) | Do not emit request.payload / query / headers from OpenAPI schemas |
Behavior:
- Paths like
/pets/{petId}becomepets/:petId. - Only
GET/POST/PUT/PATCH/DELETEare imported; other methods are skipped with a warning. - Every documented status becomes a
responses[]entry (success_200,error_404, …).nameResponseis the first 2xx (else the first status). Error responses are present but inactive until you changenameResponseor addmatch. - Response bodies prefer
example→examples→schema.example→ a minimal schema-derived example →{}. request(default on): also writes method-levelrequestfromrequestBodyand from query/header parameters (optional fields asname?). JSON, form, and multipart bodies are mapped when the schema is an object; path params stay in the route. Use--no-requestfor response-only stubs. Schemas the mock engine cannot represent are skipped with a warning (see Troubleshooting — import).- Server base path: if
servers[0].urlhas a path (e.g.https://api.nasa.gov/planetary→/planetary, or/api/v3), the import writesmock.config.jsonand puts tag files under folders that share thatprefix. Endpoint keys stay relative (apod, notplanetary/apod), soGET /planetary/apodworks at runtime. Grouping is still by OpenAPI tag, not by prefix (the server path is usually one shared prefix). - Swagger 2.0 is not supported (convert to OpenAPI 3.x first). Does not generate store CRUD or
matchin this version.
Mock file reference
Authoritative field reference for mock JSON files loaded by mock-server start.
A mock file is a JSON object. Top-level keys are endpoints (route paths). Each endpoint is an object whose keys are HTTP methods and, optionally, a sibling store definition/reference.
{
"<endpoint>": {
"store"?: StoreDefinition | { "id": string }, // endpoint level only
"GET" | "POST" | "PUT" | "PATCH" | "DELETE": Method
},
...
}Guides and samples: Examples, Advanced examples, Real-world.
Related contracts: Body compatibility, Store, Mock config, Validation.
Endpoint
| Rule | Detail |
|------|--------|
| Path pattern | Literals: letters, numbers, -, _, ., ~, /. Params like :id / :item-id (letters, numbers, _, - only — not .). |
| Methods | At least one of GET, POST, PUT, PATCH, DELETE (case as written in JSON; validated uppercased) |
| store | Optional; not an HTTP method. Sibling of methods. See Store |
Invalid characters or an endpoint with only store and no methods → startup error.
Method
Method {
nameResponse: string // required — default response name
delay?: number // ms, ≥ 0
proxy?: string | { target, path? } // URL or object; not `true`
request?: Request // see Body compatibility
responses: Response[] // required, non-empty
}| Field | Required | Notes |
|-------|----------|-------|
| nameResponse | yes | Must match a responses[].name |
| responses | yes | Non-empty array |
| delay | no | Non-negative number |
| proxy | no | http/https URL string or { "target": "<url>", "path"?: string }. true is not allowed at method level |
| request | no | Must include payload, query, and/or headers. See Body compatibility |
Response
Response {
name: string // required
statusCode?: number | string // required unless proxy is set
headers?: Record<string, string>
body?: unknown // required unless proxy or action
encoding?: "file" | "base64" // response serialization only
delay?: number
match?: Match
proxy?: string | true | { target, path? }
action?: StoreAction // list | get | create | update | patch | delete | restore
}| Field | Required | Notes |
|-------|----------|-------|
| name | yes | Unique among siblings for selection / nameResponse / error refs |
| statusCode | yes* | Numeric (or numeric string). Non-IANA codes → warning. *Optional when proxy is set |
| body | yes* | JSON/primitive payload, or path/base64 string when encoding is set. *Optional when proxy or action is set |
| headers | no | Object of string values |
| encoding | no | "file" or "base64" only. Incompatible with proxy and action |
| delay | no | Overrides method / folder / root delay for this response |
| match | no | Scenario selector (see below) |
| proxy | no | true (inherit target), URL string, or { target, path? } |
| action | no | Requires endpoint store. Incompatible with proxy. See Store actions |
One output mode per response: exactly one of proxy / action / static body (+ optional encoding) runs for the selected response.
encoding:
| Value | body | Output |
|-------|--------|--------|
| omitted | JSON / primitive | res.json(body) |
| "base64" | base64 string | decoded bytes |
| "file" | relative path under mocks root | file bytes (.. / escape → runtime 500) |
Details: Body compatibility — Response encoding.
action warnings:
bodypresent andaction≠list→ body ignored (warning)action: "delete"withstatusCode≠204→ status ignored (always204)
match
Object; must include at least one of: params, query, body, headers, multipart, call.
| Key | Shape | Notes |
|-----|-------|-------|
| params | non-empty object | Route params |
| query | non-empty object | Query string |
| body | any JSON value | Compared against parsed body |
| headers | non-empty object | Header matching |
| multipart | non-empty object | Multipart field matching |
| call | positive int or object | Call counting / sequencing |
match.call object:
| Key | Type | Notes |
|-----|------|-------|
| index | positive int (≥ 1) | Required unless reset: true |
| by | { body \| query \| params: string } | Exactly one of those three keys; scopes the counter |
| loop | boolean | When true with indexes, indexes should be contiguous 1..max (warning if not) |
| reset | boolean | Reset counter; reset-only call must also include params/query/body/headers/multipart |
Within one method, all match.call.by values must be identical (startup error otherwise).
Selection: responses with match are tried; if none match, nameResponse is used. See Advanced examples for scenarios.
proxy value shapes
| Location | Allowed |
|----------|---------|
| Response | true | URL string | { "target", "path?" } |
| Method | URL string | { "target", "path?" } (not true) |
| mock.config.json root / folder | URL string | { "target", "path?" } (not true) |
| CLI --proxy | URL string |
When response "proxy": true, target resolution order:
method proxy → folder proxy → root config proxy → CLI --proxy
If none → HTTP 502. Explicit URL / { target } on the response skips the cascade.
Folder proxyUnmatched and stripPrefix are config-level; see Mock config.
Request (summary)
request?: {
as?: "json" | "form" | "multipart" | "raw" | "text"
payload?: ...
query?: Record<string, FieldSchema>
headers?: Record<string, FieldSchema>
error?: { response?, format?, detail?, key? }
}Full contract, field rules, error formats, and 3.x → 4.x migration: Body compatibility.
Legacy keys rejected at startup: request.body, invalidResponse, errorFormat, errorDetail, errorDetailsKey.
store (endpoint-level summary)
"store": { "id": "notes", "key": "id", "seed": [], ... }or reference:
"store": { "id": "notes" }A reference is only { "id": "..." }. Full schema, actions, soft delete, relations, persist, list/filter: Store.
Delay resolution
Most specific wins:
response → method → folder (mock.config.json) → root config → 0
Headers merge (with mock.config.json)
{ ...root, ...folder, ...response } — same key → more specific wins.
Minimal example
{
"api/health": {
"GET": {
"nameResponse": "ok",
"responses": [
{
"name": "ok",
"statusCode": 200,
"body": { "ok": true }
}
]
}
}
}Validation reference
Startup validation for mock-server start. Source of truth: src/cli/commands/start/execute-mock.ts, start-mock.ts, files.ts, process-file.ts, and src/validators/*.
Order of operations
When you run mock-server start:
- Load
mock.config.json(if present under the mocks directory): parse + validate config shape. Errors are collected (not thrown yet). Used to resolve the listen port. - Resolve port: CLI
-p→ configport→3001. - Port availability (before mock routes are registered): socket check. If the port is in use, the process fails immediately without finishing mock validation.
- Discover mock files: root
*.json(exceptmock.config.json); with config, also declaredfolders/*(respectingenabled/include/exclude). Missing declared folders → errors. - Parse each mock file: must be a non-empty JSON object (syntax / empty-file errors collected here).
- Collect store definitions from endpoint-level
store(full definitions only); duplicateid→ error. Apply folderstoreNamespacewhen set. - Store relations integrity across the registry (
validateStoreRelationsIntegrity). - Per endpoint / method / response:
- Endpoint path + at least one HTTP method
- HTTP methods:
GET,POST,PUT,PATCH,DELETE - Method:
nameResponse,responses, optionaldelay/proxy/request - Response:
name,statusCode(unlessproxy),body(unlessproxyoraction),match,delay,proxy,encoding,action - Request:
payload/query/headers/as/errorshapes; legacy keys rejected - Store actions, conflict / notFound response name existence, soft-delete rules for
restore - Optional
strictDuplicatesroute ownership when enabled in config
- Emit warnings, then emit errors. Any error → throw
Invalid mock configurationand exit (server does not start).
JSON structure checks are not a final separate step: invalid JSON fails at step 5 when the file is loaded.
Errors vs warnings
| Severity | Effect | Typical causes |
|----------|--------|----------------|
| Error | Server does not start | Missing required fields, invalid structure, unknown keys, bad request / store / proxy / encoding combinations, missing named responses, relation integrity failures, invalid persist snapshots at load |
| Warning | Server still starts | Non-standard (non-IANA) statusCode; body ignored when action is set (except list); statusCode ≠ 204 on action: "delete"; non-contiguous match.call indexes when loop: true |
Error messages include file, endpoint, and method when applicable.
Watch mode
After a successful start, the mocks directory is watched (chokidar, debounce 150 ms).
| Behavior | Detail |
|----------|--------|
| Triggers | add / change / unlink under the mocks directory |
| Persist ignore | Persist snapshots are ignored so store writes do not restart the server: .store/**, custom persist.file paths, their .tmp siblings, and custom persist parent dirs (when not the mocks root) |
| --reset-store | Applied only on the initial CLI start. Watch reloads call executeMock without resetStore |
| Validation failure on reload | Restart is aborted; the process logs that the server could not be restarted and asks you to fix mocks and run the command again. It does not keep waiting in a reload loop for a later fix |
Related
Body compatibility
Contract (since 4.0.0) for multi content-type request validation, tolerant intake (multipart / form / raw / text), grouped error options, and binary mock responses (encoding).
Package version: 4.0.3. See also Advanced examples — request validation and the field summary in Mock file.
Goals
- Do not fail when the frontend sends
multipart/form-data, urlencoded, raw binary, or text — respond like the real API (usually JSON). - Validate those payloads with one clear shape: always
{ "type": "...", ... }(andformatwhen relevant). - Respond with images/PDFs/etc. via
encoding+body(no separatebodyFilekey).
Request contract
request?: {
as?: "json" | "form" | "multipart" | "raw" | "text" // omit = auto from Content-Type
payload?: PayloadSchema
query?: FieldMap
headers?: FieldMap
error?: {
response?: string // named response on validation failure
format?: "array" | "map" // default: "array"
detail?: object | string
key?: string // default: "errors"
}
}At least one of payload, query, headers is required when request is present.
Minimal (JSON):
"request": {
"payload": {
"email": { "type": "string", "format": "email" },
"password": { "type": "string", "minLength": 8 }
},
"error": {
"response": "invalid"
}
}Multipart profile upload (requires "as": "multipart" when any payload field has type: "file"; see 44-profile-body-compat.json):
"request": {
"as": "multipart",
"payload": {
"name": { "type": "string", "minLength": 2 },
"email": { "type": "string", "format": "email" },
"age?": { "type": "number", "min": 18, "max": 120 },
"avatar": { "type": "file", "format": ["png", "jpeg"] },
"banner?": { "type": "file", "format": "image/*" },
"cv?": {
"type": "file",
"format": "pdf",
"maxSize": 5000000,
"message": "CV must be a PDF up to 5MB"
}
},
"error": {
"response": "invalid",
"format": "map"
}
}Force multipart Content-Type (reject non-multipart requests first):
"request": {
"as": "multipart",
"payload": {
"title": { "type": "string" },
"file": { "type": "file", "format": "png" }
},
"error": { "response": "invalid" }
}Raw body (e.g. PUT image):
"request": {
"as": "raw",
"payload": {
"type": "file",
"format": ["png", "jpeg"],
"maxSize": 2000000
},
"error": { "response": "invalid" }
}Payload field rules (type / format)
One shape for every field: an object with type. Use format when you need a string format or a file kind. Do not use ambiguous shorthands like "email": "email" or "avatar": ["png", "jpeg"].
Optional fields: trailing ? on the key ("age?", "cv?").
Options by type
| Option | string | number | boolean | object | array | file |
|--------|----------|----------|-----------|----------|---------|--------|
| format | email, uuid, url, date | — | — | — | — | png, jpeg, webp, pdf, image/*, file, MIME, or list |
| minLength / maxLength | ✅ | — | — | — | — | — |
| pattern | ✅ | — | — | — | — | optional filename pattern |
| min / max | — | ✅ | — | — | — | — |
| enum | ✅ | ✅ | — | — | — | — |
| properties | — | — | — | ✅ | — | — |
| items | — | — | — | — | ✅ | — |
| minItems / maxItems | — | — | — | — | ✅ | multiple parts with same name |
| maxSize / minSize | — | — | — | — | — | ✅ (bytes) |
| requireFilename | — | — | — | — | — | ✅ optional |
| message | ✅ | ✅ | ✅ | ✅ | ✅ | ✅ |
| messages | ✅ per-rule map (optional) | same | same | same | same | same |
format aliases for files (resolved internally):
| User writes | Means |
|-------------|--------|
| png | image/png |
| jpeg / jpg | image/jpeg |
| webp | image/webp |
| gif | image/gif |
| pdf | application/pdf |
| image/* | any image |
| file / */* | any file part |
| image/png | used as-is |
Legacy type-only shortcut (still allowed for non-file types):
"name": "string"equals { "type": "string" }. Formats and files must use the object form.
Whole-body payload: a single rule object ({ "type": "string", ... } or { "type": "file", ... }) requires as: "text" or as: "raw" (or top-level type: "file"). Otherwise use a field map.
Content-Type detection (as)
| as | Behavior |
|------|----------|
| (omitted) | Auto: detect from the incoming Content-Type, then validate payload |
| "json" | Require JSON; else validation error → error.response |
| "form" | Require application/x-www-form-urlencoded |
| "multipart" | Require multipart/form-data |
| "raw" | Require binary/raw (image/*, application/pdf, octet-stream, audio/*, video/*, …). Also accepts other non-mapped Content-Types when as is explicitly "raw". |
| "text" | Require text/plain |
Auto mapping (detectRequestAs):
| Incoming Content-Type | Mode |
|-------------------------|------|
| application/json / +json | json |
| application/x-www-form-urlencoded | form |
| multipart/form-data | multipart |
| text/plain | text |
| image/*, application/pdf, application/octet-stream, audio/*, video/* | raw |
| no / unknown type | null (payload checks may be skipped when there is no body; query / headers still validate) |
Flow when as is set:
1) Does the frontend Content-Type match `as`?
NO → validation error (default or error.response)
YES → validate payload / query / headersWithout as: detect → validate payload for that mode.
Intake rule: if there is no request, or no file rules and no forced as, opaque bodies must not crash the server — select the mock response as usual. Body size limit: 10 MiB (RAW_BODY_LIMIT); larger bodies → 413. Multipart caps: 20 files, 100 fields.
Error object
All optional; defaults always apply.
"error": {
"response": "invalid",
"format": "map",
"detail": "{{message}}",
"key": "errors"
}| Key | Default | Role |
|-----|---------|------|
| response | generic 400 | Named response to use on failure |
| format | "array" | "array" = list of issue objects; "map" = { field: [messages] } |
| detail | built-in per format | Template(s) with {{path}}, {{rule}}, {{expected}}, {{received}}, {{message}} |
| key | "errors" | Property name where errors are injected into the response body |
Generic failure (no named response): status 400, message "Invalid request".
Message resolution per failed rule:
rule.messages[ruleName] → rule.message → library defaultResponse encoding
Response-only (how to serialize body). Not used on request.
| encoding | body means | Output |
|------------|--------------|--------|
| (omitted) | JSON / primitive | res.json(body) |
| "base64" | base64 string | decoded bytes |
| "file" | relative path under mocks root | file bytes (paths with .. rejected) |
{
"name": "avatar",
"statusCode": 200,
"headers": { "Content-Type": "image/png" },
"encoding": "file",
"body": "fixtures/avatar.png"
}{
"name": "tiny",
"statusCode": 200,
"headers": { "Content-Type": "image/png" },
"encoding": "base64",
"body": "iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mP8z8BQDwAEhQGAhKmMIQAAAABJRU5ErkJggg=="
}What you can do
| Goal | How |
|------|-----|
| Serve a local image/PDF/binary from the mocks folder | "encoding": "file", "body": "assets/avatar.png" (+ usually Content-Type) |
| Serve bytes embedded in the mock JSON | "encoding": "base64", "body": "<base64>" |
| Keep classic JSON mocks | Omit encoding (default res.json) |
| Choose binary vs proxy vs store by scenario | Separate responses (e.g. different match / nameResponse) — one mode per response |
| Validate upload then return JSON | request (multipart/file) + normal JSON body (no encoding required) |
| Proxy multipart/binary upstream unchanged | proxy on the response (uses rawBody; do not set encoding on that same response) |
What you cannot do (startup error)
One response = one output mode. Mixing them on the same response fails validation:
| Combination | Result |
|-------------|--------|
| encoding + proxy | ❌ config error |
| encoding + action | ❌ config error |
| proxy + action | ❌ config error |
| encoding not file / base64 | ❌ config error |
| encoding set but body is not a string | ❌ config error |
| encoding: "file" with empty / whitespace body | ❌ config error |
| request.body / flat invalidResponse / … | ❌ config error (use payload / error.*) |
encoding is not ignored when proxy is present: the server refuses to start so dead config is not silent.
Runtime failures (encoding: "file" / "base64")
These pass config validation but fail when the response is selected:
| Situation | HTTP | Behavior |
|-----------|------|----------|
| File path missing under mocks root | 500 | JSON { "message": "…" } — Content-Type: application/json |
| Path escapes mocks root (../…) | 500 | Response body file path escapes mocks directory: … |
Request side (related)
| Allowed | Not allowed / notes |
|---------|---------------------|
| payload + optional as, query, headers, error | Legacy body, invalidResponse, errorFormat, errorDetail, errorDetailsKey |
| as: "json" \| "form" \| "multipart" \| "raw" \| "text" | If as is set and Content-Type does not match → validation error → error.response (or generic 400) |
| Whole-body rule object | Requires as: "text" or as: "raw" (or top-level type: "file") |
| Field rules per type (see Options by type) | File shorthand like "avatar": ["png"]; use { "type": "file", "format": … } |
| Form / multipart coerce number / boolean from strings | — |
| match.headers / match.multipart after validation passes | Empty / non-object match.headers / match.multipart → startup error |
Pipeline (body compatibility)
incoming request
→ tolerant intake (rawBody when needed)
→ request?
→ as? check Content-Type
→ parse (json | form | multipart | raw | text)
→ validate payload / query / headers
FAIL → error.response (or generic 400)
PASS → match → delay → proxy | action | encoding/bodyExactly one of proxy / action / static body(+optional encoding) runs for the selected response.request = “is this valid?” · match = “which scenario?” (match.headers / match.multipart included). They do not replace each other.
Proxy value shapes and inheritance: Mock file — proxy, Mock config. Tutorials: Advanced examples, Real-world.
Migration from 3.x
| Removed (3.x) | Use in 4.0 |
|---------------|------------|
| request.body | request.payload |
| invalidResponse | error.response |
| errorFormat | error.format |
| errorDetail | error.detail |
| errorDetailsKey | error.key |
There is no dual-read / alias period: legacy keys are rejected at startup with a clear error.
Out of scope: response multipart builder, GraphQL/XML.
Folder organization (mock.config.json): Mock config.
Mutable store: Store.
Mock config reference
Optional folder organization for large mock sets (since 1.18.0). Put mock.config.json at the root of the mocks directory passed to --path.
Repo sample: mocks/mock-config/ (food delivery: auth, orders, payments). See also Examples, Real-world.
Without this file, mocks work as flat JSON files in the mocks directory.
File location
<mocks-dir>/
mock.config.json
health.json # root JSON still loads
auth/
login.json
orders/
list.jsonFilename is always mock.config.json (MOCK_CONFIG_FILENAME).
Root options
| Option | Type | Default | Purpose |
|--------|------|---------|---------|
| delay | number (≥ 0) | — | Default latency (ms) for root and folder files (folder may override) |
| proxy | URL string | { target, path? } | — | Default proxy target. Not true |
| headers | Record<string, string> | — | Default response headers |
| strictDuplicates | boolean | false | When true, startup fails if the same HTTP method + final route is registered twice |
| port | integer 1–65535 | 3001 (effective) | Default listen port. CLI -p / --port overrides |
| folders | object | — | Declared subfolders and their settings |
prefix is not allowed at root — only inside folders (startup error if present).
Folder options (folders.<name>)
Folder name: letters, numbers, -, _, . only. One level under the mocks root (e.g. auth/*.json). Only folders declared in folders are loaded; undeclared directories are ignored.
| Option | Type | Default | Purpose |
|--------|------|---------|---------|
| prefix | string path | — | Route prefix for every endpoint in that folder (/api/users + login → /api/users/login). No route parameters (:id). Leading/trailing / normalized |
| delay | number (≥ 0) | — | Overrides root delay for files in this folder |
| proxy | URL string | { target, path? } | — | Overrides root proxy. Not true |
| headers | Record<string, string> | — | Merged with root headers |
| enabled | boolean | true | false skips the folder (also ignored by strictDuplicates) |
| include | string[] | all .json | Basename patterns (*, ?); if set, only matching files load |
| exclude | string[] | none | Basename patterns skipped after include (e.g. *-draft.json) |
| stripPrefix | boolean | false | When proxying, remove folder prefix from the upstream path. Requires prefix |
| proxyUnmatched | http/https URL | — | Catch-all proxy for requests under this folder prefix with no mock route. Requires prefix |
| storeNamespace | string | — | Prefixes store ids (session → users:session). Pattern: letters, numbers, -, _, .. Ids that already contain : are left as-is; relation targets in the same folder are namespaced too |
Declared folder missing on disk → startup error.
Discovery rules
- Always load root-level
*.jsonexceptmock.config.json. - If
foldersis set, for each declared folder withenabled !== false, load matching*.jsonfiles (include/exclude). - Nested folders deeper than one level are not scanned.
mock-server add/initstill write to the root of the mocks directory.
Priority cheat-sheet
| Concern | Order |
|---------|-------|
| port | CLI -p → config port → 3001 |
| delay | response → method → folder → root → 0 |
| proxy when response is true | method → folder → root config → CLI --proxy |
| headers | merge { ...root, ...folder, ...response } (same key → more specific wins) |
Explicit response proxy URL / { target } skips the cascade.
Folder proxyUnmatched only covers unmatched routes under that prefix; CLI --proxy still catch-alls remaining unmatched routes.
Basic layout
{
"delay": 100,
"headers": {
"X-Mock-Env": "local"
},
"folders": {
"users": {
"prefix": "/api/users",
"delay": 200,
"headers": {
"X-Service": "users"
},
"include": ["auth.json", "profile.json"],
"exclude": ["*-draft.json"]
},
"users-v2": {
"prefix": "/api/users",
"enabled": false
}
}
}users/auth.json with endpoint login → POST /api/users/login (prefix + folder delay/headers merged with response headers).
strictDuplicates
{
"strictDuplicates": true,
"folders": {
"users": { "prefix": "/api/users" },
"auth": { "prefix": "/api/users" }
}
}If both define POST login, startup fails (duplicate final route).
port
{
"port": 3500,
"folders": {
"users": { "prefix": "/api/users" }
}
}mock-server start # 3500 from config
mock-server start -p 4000 # 4000 (CLI wins)delay overrides
| Request context | Delay used |
|-----------------|------------|
| Response has delay | that value |
| Else method has delay | method value |
| Else folder / root config | folder then root |
| None | 0 |
headers merge
Final headers = { ...root, ...folder, ...response }.
proxy: true on a response
See Mock file — proxy. Orphan proxy: true with no method/folder/root/CLI target → 502.
stripPrefix
{
"folders": {
"users": {
"prefix": "/api/users",
"proxy": "http://localhost:4000",
"stripPrefix": true
}
}
}Incoming GET /api/users/42 with "proxy": true → upstream http://localhost:4000/42.
proxyUnmatched
{
"folders": {
"users": {
"prefix": "/api/users",
"stripPrefix": true,
"proxyUnmatched": "http://localhost:4000"
}
}
}Mocked routes under the prefix stay local; other paths under the prefix proxy to the target (with strip when enabled). Outside the prefix: use CLI --proxy.
storeNamespace
{
"folders": {
"users": { "prefix": "/api/users", "storeNamespace": "users" },
"orders": { "prefix": "/api/orders", "storeNamespace": "orders" }
}
}Local "id": "session" becomes runtime users:session. Same-folder references keep writing "id": "session". Cross-folder relations use the full id ("store": "users:session").
--reset-store must use the runtime id:
mock-server start --reset-store users:session
mock-server start --reset-storeRelated
Store reference
Opt-in feature (≥ 1.11.0; advanced list filters ≥ 1.12.0; composite unique ≥ 1.13.0; soft delete ≥ 1.14.0; relations ≥ 1.15.0; customizable notFound ≥ 1.16.0; list-on-by-default ≥ 3.0.0). Without store + action, mocks stay static.
Data lives in memory for the process lifetime; optionally survive restarts with persist.
This section is the store field and behavior reference (actions, definition vs reference, soft delete, relations, persist, list/filter, statuses). Store-backed product walkthroughs C–R live in Store recipes. Minimal samples: Example A and Example B below. For multi-feature / multipart / folder scenarios, see Real-world. Also: Examples, CLI --reset-store.
Capability map
| You need… | Use | Deep dive |
|-----------|-----|-----------|
| Collection + CRUD | store + action | Actions, Schema |
| Soft delete / trash | store.softDelete | Soft delete |
| Relations / FK | store.relations | Relations |
| Auto ids / defaults | key, template | Key generation |
| Seed data | seed | Schema |
| Business uniqueness | unique + 409 | Conflicts |
| Custom missing item | store.notFound | Not found |
| Survive restart | persist / --reset-store | Persist and restart |
| Validate payload/query | request | Body compatibility |
| Branch by params/query/body/call | match | Mock file — match, Advanced examples |
| Page / offset / cursor lists | store.list | List sort and pagination |
| Filters / search / multi-sort | store.list.filter / sort | Filters / search, Multi-sort |
| Custom list JSON | list placeholders | Response templates |
| Forward to real API | proxy (not with action) | Mock file — proxy |
| Long product recipes (todo, SaaS, e-commerce, auth, …) | — | Store recipes (Examples C–R) |
Pipeline (every request): request → match → delay → exactly one of proxy / action / static body (optional encoding).
List pipeline (inside action: "list" + store.list): key params → fields (AND) → or → search → sort → page/offset/cursor → templates.
How it works
- Put
storeat endpoint level (sibling ofGET/POST/ …, not inside a method). - Define the collection once (full object with
id, optionalkey/seed/template/unique/persist). - Other routes that share data use the reference form:
"store": { "id": "notes" }. - On each response you want to mutate/read the collection, set
"action": "list" | "get" | "create" | "update" | "patch" | "delete" | "restore".
Request pipeline (fixed order):
requestvalidation (if any) → may returnerror.responseand never hits the storematch→ picks a response (nameResponsefallback)delay(once)- Exactly one of
proxy/action/ staticbody(+optionalencoding) on the selected response
Actions
| Action | Behavior | Success status | Common errors |
|--------|----------|----------------|---------------|
| list | Returns items. Filters by route key params. List engine on by default (omit / true / object): filter (fields/or/search) → multi-sort → page/offset/cursor. "list": false returns a plain full array. Optional body/headers templates. Soft-deleted items omitted unless ?includeDeleted=true | response statusCode | 400 (invalid page/sort/order/cursor/filter query) |
| get | Loads one item; all key fields must come from route params. Soft-deleted → 404 unless ?includeDeleted=true | response statusCode | 404 (default or store.notFound) |
| create | Inserts; merges template + body; auto-generates missing numeric key fields; route params override key fields. Soft-deleted rows do not count toward unique/key | typically 201 | 400 (body not object), 409 (conflict) |
| update | Full replace (template + body), preserves existing key. Soft-deleted → 404 | response statusCode | 404 / 400 / 409 |
| patch | Partial merge on existing item (no template), preserves key. Soft-deleted → 404 | response statusCode | 404 / 400 / 409 |
| delete | Without softDelete: removes item. With softDelete: sets the delete field (ISO) and keeps the row. Runs relations.onDelete on dependents first. Always 204 with empty body on success (statusCode in JSON is ignored, warning if ≠ 204) | 204 | 404 / 409 (restrict) |
| restore | Requires store.softDelete. Clears the delete field and returns the item. Soft-deleted-only; active or missing → 404 | response statusCode | 404 / 409 |
Rules for action:
- Requires
storeon the endpoint - Cannot be combined with
proxyon the same response bodyis optional; ignored with warning for actions other thanlist- For
list,bodyandheadersmay be templates with placeholders (see below) - Returned items are clones (mutating the HTTP response does not mutate the store)
restoreis only valid when the store definition hassoftDelete
Soft delete
Soft delete means: the item stays in the collection, but is marked deleted (default field deletedAt = ISO timestamp). Clients that call list / get / update / patch treat it as gone, unless they ask for trash with ?includeDeleted=true (or 1). action: "restore" clears the mark.
Why it lives on store, not on the HTTP DELETE
softDelete is a property of the collection, not of one route:
- Same data, many verbs. After a soft delete,
listmust hide the row,get/patchmust 404, anduniquemust free the email/title. That logic belongs to the store that holds the rows, not to theDELETEresponse alone. DELETEonly triggers the action. The HTTP method still uses"action": "delete". With soft delete on, that action marks; without it, that action removes. Same verb, different store policy.- One policy per
store.id. Every endpoint that references{ "id": "notes" }shares the same in-memory Map. Putting soft delete on the store definition once keeps list/get/delete/restore consistent. Putting it only on theDELETEresponse would leave other actions unaware.
So: configure "softDelete": true (or { "field": "deletedAt" }) on the full store definition. The DELETE endpoint does not need a special soft-delete flag — only "action": "delete" and a store that already has soft delete enabled.
Why a single endpoint is enough
You do not need GET list + POST create + DELETE for soft delete to work. Soft delete only needs:
- A store definition with
softDelete(and usuallyseed, or acreateroute, so there is something to delete). - A response with
"action": "delete".
Example — delete-only mock:
"api/notes/:id": {
"store": {
"id": "notes",
"softDelete": true,
"seed": [{ "id": 1, "title": "Keep" }]
},
"DELETE": {
"nameResponse": "remove",
"responses": [
{ "name": "remove", "statusCode": 204, "action": "delete" }
]
}
}DELETE /api/notes/1 returns 204 and sets deletedAt on that row. There is no list route here, so you cannot “see” the trash over HTTP until you add get/list/restore — but the soft delete did run in the store.
When you do share the store across routes (api/notes + api/notes/:id), keep one full definition (with softDelete) and use { "id": "notes" } elsewhere — see Schema (definition vs reference). Do not add softDelete on a reference; that becomes a second definition and fails startup.
Behavior
"store": {
"id": "notes",
"softDelete": true
}or "softDelete": { "field": "deletedAt" }.
| | Without softDelete | With softDelete: true |
|--|----------------------|-------------------------|
| action: "delete" | Removes the item | Keeps the item and sets deletedAt (ISO) |
| list / get | Normal | Soft-deleted items hidden (unless ?includeDeleted=true or 1) |
| update / patch | Normal | Soft-deleted → 404 |
| unique / key | Counts all items | Soft-deleted ignored (values free to reuse) |
| action: "restore" | Invalid | Clears deletedAt and returns the item |
Also:
- Absent/
nulldelete field = active. Soft-deleting an already soft-deleted item →404. - Persist keeps soft-deleted rows as-is (same file format).
Relations
Opt-in links between stores: type: "one" (FK) and type: "many" (reverse embed). Targets may use a simple or composite store key.
type: "one" (default)
"relations": {
"userId": {
"store": "users",
"join": { "from": "userId", "to": "id" },
"required": true,
"onDelete": {
"action": "restrict",
"conflict": { "response": "has-posts" }
},
"embed": { "as": "user" },
"conflict": {
"response": "invalid-user",
"detail": { "field": "{{field}}", "value": "{{value}}" }
}
},
"orderRef": {
"store": "orders",
"join": {
"from": ["tenantId", "orderId"],
"to": ["ten