npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

Readme

npm version npm downloads license GitHub stars CI E2E npm audit TypeScript Node

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, or proxy only when you need them
  • Presetsmock-server add --preset … scaffolds common shapes (CRUD, auth, upload, relations…) so you edit JSON instead of inventing structure
  • Record & Replay — point --proxy at 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):

Later (when you need it):

Reference (lookup when needed):

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 so init can add a start script)

Steps

1. Install

npm install http-mock-json --save-dev

2. Initialize

npx mock-server init

By default this will:

  1. Create a mocks directory
  2. Add a mock:start script (mock-server start)
  3. 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 start

Or 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/animals

You 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 .json file
  • Server at http://localhost:3001
  • curl returns the nameResponse body
  • Editing nameResponse (or bodies) changes the next response after reload

Next steps

  1. Concepts — how nameResponse, match, request, store, and proxy fit 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 string
  • body — parsed JSON or urlencoded fields
  • headers — header names (case-insensitive)
  • multipart — multipart fields / file metadata
  • call — N-th hit (1-based), with optional scope, loop, and reset

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 (not request.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 --proxy and folder mock.config.json targets for "proxy": true and 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.

See mock file and CLI.

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:

  1. Parse the body when needed (including multipart). Parse failures can become a request error response if request is configured; otherwise they return a client error.
  2. request validation — if configured and issues are found, serve the request-error response and stop.
  3. Response selection — walk responses in order; first entry with a satisfied match wins; otherwise use nameResponse.
  4. Delay — method-level and/or response-level latency.
  5. Fulfill:
    • proxy → forward to the resolved target
    • action + 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

  1. Examples — copy-paste samples from this repository
  2. 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 -p depends on the command: on init / add / import, -p is --path (mocks directory). On start, -p is --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 false

start

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 165535). 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:session

Record & 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 start

With 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 / --portmock.config.json port3001.

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. parent api/users writes api/users + api/posts (stores users / posts, FK userId, reverse embed, delete restrict).
  • If that default would collide with the parent (same path or same store.id — e.g. parent api/posts), the CLI asks for the child collection name (suggestion comments) 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 parent store.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/mocks

add / 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} become pets/:petId.
  • Only GET / POST / PUT / PATCH / DELETE are imported; other methods are skipped with a warning.
  • Every documented status becomes a responses[] entry (success_200, error_404, …). nameResponse is the first 2xx (else the first status). Error responses are present but inactive until you change nameResponse or add match.
  • Response bodies prefer exampleexamplesschema.example → a minimal schema-derived example → {}.
  • request (default on): also writes method-level request from requestBody and from query/header parameters (optional fields as name?). JSON, form, and multipart bodies are mapped when the schema is an object; path params stay in the route. Use --no-request for response-only stubs. Schemas the mock engine cannot represent are skipped with a warning (see Troubleshooting — import).
  • Server base path: if servers[0].url has a path (e.g. https://api.nasa.gov/planetary/planetary, or /api/v3), the import writes mock.config.json and puts tag files under folders that share that prefix. Endpoint keys stay relative (apod, not planetary/apod), so GET /planetary/apod works 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 match in 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:

  • body present and actionlist → body ignored (warning)
  • action: "delete" with statusCode204 → status ignored (always 204)

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:

  1. 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.
  2. Resolve port: CLI -p → config port3001.
  3. Port availability (before mock routes are registered): socket check. If the port is in use, the process fails immediately without finishing mock validation.
  4. Discover mock files: root *.json (except mock.config.json); with config, also declared folders/* (respecting enabled / include / exclude). Missing declared folders → errors.
  5. Parse each mock file: must be a non-empty JSON object (syntax / empty-file errors collected here).
  6. Collect store definitions from endpoint-level store (full definitions only); duplicate id → error. Apply folder storeNamespace when set.
  7. Store relations integrity across the registry (validateStoreRelationsIntegrity).
  8. Per endpoint / method / response:
    • Endpoint path + at least one HTTP method
    • HTTP methods: GET, POST, PUT, PATCH, DELETE
    • Method: nameResponse, responses, optional delay / proxy / request
    • Response: name, statusCode (unless proxy), body (unless proxy or action), match, delay, proxy, encoding, action
    • Request: payload / query / headers / as / error shapes; legacy keys rejected
    • Store actions, conflict / notFound response name existence, soft-delete rules for restore
    • Optional strictDuplicates route ownership when enabled in config
  9. Emit warnings, then emit errors. Any error → throw Invalid mock configuration and 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

  1. Do not fail when the frontend sends multipart/form-data, urlencoded, raw binary, or text — respond like the real API (usually JSON).
  2. Validate those payloads with one clear shape: always { "type": "...", ... } (and format when relevant).
  3. Respond with images/PDFs/etc. via encoding + body (no separate bodyFile key).

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 / headers

Without 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 default

Response 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/body

Exactly 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.json

Filename 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 165535 | 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 (sessionusers: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

  1. Always load root-level *.json except mock.config.json.
  2. If folders is set, for each declared folder with enabled !== false, load matching *.json files (include / exclude).
  3. Nested folders deeper than one level are not scanned.
  4. mock-server add / init still write to the root of the mocks directory.

Priority cheat-sheet

| Concern | Order | |---------|-------| | port | CLI -p → config port3001 | | 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 loginPOST /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-store

See Store, CLI.


Related

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 notFound1.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): requestmatchdelay → exactly one of proxy / action / static body (optional encoding).
List pipeline (inside action: "list" + store.list): key params → fields (AND) → orsearch → sort → page/offset/cursor → templates.

How it works

  1. Put store at endpoint level (sibling of GET / POST / …, not inside a method).
  2. Define the collection once (full object with id, optional key / seed / template / unique / persist).
  3. Other routes that share data use the reference form: "store": { "id": "notes" }.
  4. On each response you want to mutate/read the collection, set "action": "list" | "get" | "create" | "update" | "patch" | "delete" | "restore".

Request pipeline (fixed order):

  1. request validation (if any) → may return error.response and never hits the store
  2. match → picks a response (nameResponse fallback)
  3. delay (once)
  4. Exactly one of proxy / action / static body(+optional encoding) 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 store on the endpoint
  • Cannot be combined with proxy on the same response
  • body is optional; ignored with warning for actions other than list
  • For list, body and headers may be templates with placeholders (see below)
  • Returned items are clones (mutating the HTTP response does not mutate the store)
  • restore is only valid when the store definition has softDelete

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:

  1. Same data, many verbs. After a soft delete, list must hide the row, get/patch must 404, and unique must free the email/title. That logic belongs to the store that holds the rows, not to the DELETE response alone.
  2. DELETE only 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.
  3. 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 the DELETE response 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:

  1. A store definition with softDelete (and usually seed, or a create route, so there is something to delete).
  2. 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/null delete 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