mockaton
Version:
HTTP Mock Server
899 lines (648 loc) • 23.4 kB
Markdown
<img src="src/client/logo.svg" alt="Mockaton Logo" width="210" style="margin-top: 30px"/>

[](https://github.com/ericfortis/mockaton/actions/workflows/test.yml)
[](https://codecov.io/github/ericfortis/mockaton)
An HTTP mock server for simulating APIs with minimal setup — ideal
for testing difficult to reproduce backend states.
## Overview
With Mockaton, you don’t need to write code for wiring up your
mocks. Instead, a given directory is scanned for filenames
following a convention similar to the URLs.
For example, for [/api/company/123](#), the filename could be:
<pre>
<code>my-mocks-dir/<b>api/company</b>/[id].GET.200.json</code>
</pre>
<br/>
## Quick Start (Docker)
This will spin up Mockaton with the sample directories
included in this repo mounted on the container.
_[mockaton-mocks/](./mockaton-mocks) and [mockaton-static-mocks/](./mockaton-static-mocks)_
```sh
git clone https://github.com/ericfortis/mockaton.git --depth 1
cd mockaton
make docker
```
Dashboard: http://localhost:2020/mockaton
Test it:
```shell
curl localhost:2020/api/user
```
<br/>
## Dashboard
On the dashboard you can:
- Select a mock variant for a particular route
- 🕓 Delay responses
- Trigger an autogenerated `500` error
- …and cycle it off (for testing retries)
Nonetheless, there’s a programmatic API, which is handy for
setting up tests (see **Commander API** section below).
<picture>
<source media="(prefers-color-scheme: light)" srcset="pixaton-tests/tests/macos/pic-for-readme.vp761x740.light.gold.png">
<source media="(prefers-color-scheme: dark)" srcset="pixaton-tests/tests/macos/pic-for-readme.vp761x740.dark.gold.png">
<img alt="Mockaton Dashboard" src="pixaton-tests/tests/macos/pic-for-readme.vp761x740.light.gold.png">
</picture>
<br/>
<br/>
## Multiple Mock Variants
Each route can have different mocks. There are two
options for doing that, and they can be combined.
### Adding comments to the filename
Comments are anything within parentheses, including them.
<pre>
api/login<b>(locked out user)</b>.POST.423.json
api/login<b>(invalid login attempt)</b>.POST.401.json
</pre>
You can **Bulk Select** mocks by comments to simulate the complete states
you want. For example:
<img src="docs/changelog/bulk-select.png" width="180px">
### Different response status code
For instance, you can use a `4xx` or `5xx` status code for triggering error
responses, or a `2xx` such as `204` for testing empty collections.
<pre>
api/videos.GET.<b>204</b>.empty # No Content
api/videos.GET.<b>403</b>.json # Forbidden
api/videos.GET.<b>500</b>.txt # Internal Server Error
</pre>
<br/>
## Scraping Mocks from your Backend
### Option 1: Browser extension
The companion Chrome [devtools extension](https://chromewebstore.google.com/detail/mockaton-downloader/babjpljmacbefcmlomjedmgmkecnmlaa)
lets you download all the HTTP responses and
save them in bulk following Mockaton’s filename convention.

### Option 2: Fallback to your backend
<details>
<summary>Learn more…</summary>
This option could be a bit elaborate if your backend uses third-party authentication,
because you’d have to manually inject cookies or `sessionStorage` tokens.
On the other hand, proxying to your backend is straightforward if your backend
handles the session cookie, or if you can develop without auth.
Either way you can forward requests to your backend for routes you don’t have
mocks for, or routes that have the ☁️ **Cloud Checkbox** checked. In addition, by
checking ✅ **Save Mocks**, you can collect the responses that hit your backend.
They will be saved in your `config.mocksDir` following the filename convention.
</details>
<br/>
<br/>
## Motivation
### Deterministic and comprehensive states
Sometimes the flow you need to test is
too difficult to reproduce from the actual backend.
- Demo edge cases to PMs, Design, and clients
- Set up screenshot tests (see [pixaton-tests/](pixaton-tests) in this repo)
- Spot inadvertent regressions during development. For example, the demo
app in this repo has a list of colors containing all of their possible
states. This way you’ll indirectly notice if something broke.
<img src="./demo-app-vite/pixaton-tests/pic-for-readme.vp740x880.light.gold.png" alt="Mockaton Demo App Screenshot" width="740" />
<br/>
## Benefits
### Standalone demo server (Docker)
You can demo your app by compiling the frontend and putting
its built assets in `config.staticDir`. For example, this
repo includes a demo which builds and runs a docker container.
```sh
git clone https://github.com/ericfortis/mockaton.git --depth 1
cd mockaton/demo-app-vite
make run-standalone-demo
```
- App: http://localhost:4040
- Dashboard: http://localhost:4040/mockaton
### Testing scenarios that would otherwise be skipped
- Trigger dynamic states on an API. For example, for polled alerts or notifications.
- Testing retries, you can change an endpoint from a 500 to a 200 on the fly.
- Simulate errors on third-party APIs, or on your project’s backend.
- Generate dynamic responses. Use Node’s HTTP handlers (see function mocks below). So you can, e.g.:
- have an in-memory database
- read from disk
- read query string
- pretty much anything you can do with a normal backend request handler
### Privacy and security
- Does not write to disk. Except when you select ✅ **Save Mocks** for scraping mocks from a backend
- Does not initiate network connections (no logs, no telemetry)
- Does not hijack your HTTP client
- Auditable
- Organized and small. 4 KLoC (half is UI and tests)
- Zero dependencies. No runtime and no build packages.
<br/>
## Benefits of Mocking APIs in General
The section above highlights benefits specific to Mockaton. There are more, but
in general here are some benefits which Mockaton has but other tools have as well:
### Works around unstable dev backends while developing UIs
- Syncing the database and spinning up dev infrastructure can be complex
- Mitigates progress from being blocked by waiting for APIs
### Time travel
If you commit the mocks to your repo, you don’t have to downgrade
backends when checking out long-lived branches or bisecting bugs.
<br/>
## Usage (without Docker)
_For Docker, see the Quick-Start section above._
Requires Node.js **v22.18+**, which supports TypeScript mocks.
1. Create a mock in the default directory (`./mockaton-mocks`)
```sh
mkdir -p mockaton-mocks/api
echo "[1,2,3]" > mockaton-mocks/api/foo.GET.200.json
```
2. Run Mockaton (`npx` comes with Node, and installs Mockaton if needed)
```shell
npx mockaton --port 4040
```
3. Test it
```shell
curl localhost:4040/api/foo
```
## Or, on Node Projects
```sh
npm install mockaton --save-dev
```
In your `package.json`:
```json
"scripts": {
"mockaton": "mockaton --port 4040"
}
```
<br/>
## CLI Options
The CLI options override their counterparts in `mockaton.config.js`
```txt
-c, --config <file> (default: ./mockaton.config.js)
-m, --mocks-dir <dir> (default: ./mockaton-mocks/)
-s, --static-dir <dir> (default: ./mockaton-static-mocks/)
-H, --host <host> (default: 127.0.0.1)
-p, --port <port> (default: 0) which means auto-assigned
-q, --quiet Errors only
--no-open Don’t open dashboard in a browser
-h, --help
-v, --version
```
## mockaton.config.js (Optional)
Mockaton looks for a file `mockaton.config.js` in its current working directory.
### Defaults Overview
The next section has the documentation, but here's an overview of the defaults:
```js
import { defineConfig, jsToJsonPlugin, openInBrowser, SUPPORTED_METHODS } from 'mockaton'
export default defineConfig({
mocksDir: 'mockaton-mocks',
staticDir: 'mockaton-static-mocks',
ignore: /(\.DS_Store|~)$/,
watcherEnabled: true,
host: '127.0.0.1',
port: 0, // auto-assigned
logLevel: 'normal',
delay: 1200, // ms. Applies to routes with the Delay Checkbox "ON"
delayJitter: 0,
proxyFallback: '',
collectProxied: false,
formatCollectedJSON: true,
cookies: {},
extraHeaders: [],
extraMimes: {},
corsAllowed: true,
corsOrigins: ['*'],
corsMethods: SUPPORTED_METHODS,
corsHeaders: ['content-type', 'authorization'],
corsExposedHeaders: [],
corsCredentials: true,
corsMaxAge: 0,
plugins: [
[/\.(js|ts)$/, jsToJsonPlugin]
],
onReady: await openInBrowser
})
```
<br/>
<details>
<summary><b>Config Documentation…</b></summary>
### `mocksDir?: string`
Defaults to `'mockaton-mocks'`.
### `staticDir?: string`
Defaults to `'mockaton-static-mocks'`.
This option is not needed besides serving partial content (e.g., videos). But
it’s convenient for serving 200 GET requests without having to add the filename
extension convention. For example, for using Mockaton as a standalone demo server,
as explained above in the _Use Cases_ section.
Files under `config.staticDir` take precedence over corresponding
`GET` mocks in `config.mocksDir` (regardless of status code).
For example, if you have two files for `GET` <a href="#">/foo/bar.jpg</a> such as:
<pre>
my-static-dir<b>/foo/bar.jpg</b> <span style="color:green"> // Wins</span>
my-mocks-dir<b>/foo/bar.jpg</b>.GET.200.jpg <span style="color:red"> // Unreachable</span>
</pre>
<br/>
### `ignore?: RegExp`
Defaults to `/(\.DS_Store|~)$/`
The regex rule is tested against the basename (filename without directory path).
<br/>
### `watcherEnabled?: boolean`
Defaults to `true`
When `false`, if you **add**, **delete**, or **rename** you’ll need to click **"Reset"**
on the Dashboard, or call `commander.reset()` in order to re-initialize the collection.
On the other hand, **edits are not affected by this
flag**; mocks are always read from disk on every request.
<br/>
### `host?: string`
Defaults to `'127.0.0.1'`
### `port?: number`
Defaults to `0`, which means auto-assigned
<br/>
### `delay?: number`
Defaults to `1200` milliseconds. Although routes can individually be delayed
with the 🕓 Checkbox, the amount is globally configurable with this option.
### `delayJitter?: number`
Defaults to `0`. Range: `[0.0, 3.0]`. Maximum percentage of the delay to add.
For example, `0.5` will add at most `600ms` to the default delay.
<br/>
### `proxyFallback?: string`
For example, `config.proxyFallback = 'http://example.com'`
Lets you specify a target server for serving routes you don’t have mocks for,
or that you manually picked with the ☁️ **Cloud Checkbox**.
### `collectProxied?: boolean`
Defaults to `false`. With this flag you can save mocks that hit
your proxy fallback to `config.mocksDir`. If the URL has v4 UUIDs,
the filename will have `[id]` in their place. For example:
<pre>
<b>/api/user/</b>d14e09c8-d970-4b07-be42-b2f4ee22f0a6<b>/likes</b> =>
my-mocks-dir<b>/api/user/</b>[id]<b>/likes</b>.GET.200.json
</pre>
Your existing mocks won’t be overwritten. Responses of routes with
the ☁️ **Cloud Checkbox** selected will be saved with unique filename-comments.
<details>
<summary>Extension details</summary>
<p>
An <code>.empty</code> extension means the <code>Content-Type</code>
header was not sent by your backend.
</p>
<p>
An <code>.unknown</code> extension means the <code>Content-Type</code> is not in
the predefined list. For that, you can add it to <code>config.extraMimes</code>
</p>
</details>
### `formatCollectedJSON?: boolean`
Defaults to `true`. Saves the mock with two spaces indentation —
the formatting output of `JSON.stringify(data, null, ' ')`
<br/>
### `cookies?: { [label: string]: string }`
```js
import { jwtCookie } from 'mockaton'
config.cookies = {
'My Admin User': 'my-cookie=1;Path=/;SameSite=strict',
'My Normal User': 'my-cookie=0;Path=/;SameSite=strict',
'My JWT': jwtCookie('my-cookie', {
name: 'John Doe',
picture: 'https://cdn.auth0.com/avatars/jd.png'
}),
'None': ''
}
```
The selected cookie, which is the first one by default, is sent in every response in a
`Set-Cookie` header (as long as its value is not an empty string). The object key is just
a label for UI display purposes, and also for selecting a cookie via the Commander API.
If you need to send more than one cookie, you can inject them globally
in `config.extraHeaders`, or individually in a function `.js` or `.ts` mock.
By the way, the `jwtCookie` helper has a hardcoded header and signature.
So it’s useful only if you care about its payload.
<br/>
### `extraHeaders?: string[]`
Note: it’s a one-dimensional array. The header name goes at even indices.
```js
config.extraHeaders = [
'Server', 'Mockaton',
'Set-Cookie', 'foo=FOO;Path=/;SameSite=strict',
'Set-Cookie', 'bar=BAR;Path=/;SameSite=strict'
]
```
<br/>
### `extraMimes?: { [fileExt: string]: string }`
```js
config.extraMimes = {
jpe: 'application/jpeg',
html: 'text/html; charset=utf-8' // overrides built-in
}
```
Those extra media types take precedence over the built-in
[utils/mime.js](src/server/utils/mime.js), so you can override them.
<br/>
### `plugins?: [filenameTester: RegExp, plugin: Plugin][]`
```ts
type Plugin = (
filePath: string,
request: IncomingMessage,
response: OutgoingMessage
) => Promise<{
mime: string,
body: string | Uint8Array
}>
```
Plugins are for processing mocks before sending them. If no
regex matches the filename, the fallback plugin will read
the file from disk and compute the MIME from the extension.
Note: don’t call `response.end()` on any plugin.
<details>
<summary><b> See plugin examples </b></summary>
```shell
npm install yaml
```
```js
import { parse } from 'yaml'
import { readFileSync } from 'node:js'
import { jsToJsonPlugin } from 'mockaton'
config.plugins = [
// Although `jsToJsonPlugin` is set by default, you need to include it if you need it.
// IOW, your plugins array overwrites the default list. This way you can remove it.
[/\.(js|ts)$/, jsToJsonPlugin],
[/\.yml$/, yamlToJsonPlugin],
// e.g. GET /api/foo would be capitalized
[/foo\.GET\.200\.txt$/, capitalizePlugin]
]
function yamlToJsonPlugin(filePath) {
return {
mime: 'application/json',
body: JSON.stringify(parse(readFileSync(filePath, 'utf8')))
}
}
function capitalizePlugin(filePath) {
return {
mime: 'application/text',
body: readFileSync(filePath, 'utf8').toUpperCase()
}
}
```
</details>
<br/>
### `corsAllowed?: boolean`
Defaults to `true`. When `true`, these are the default options:
<details>
<summary>CORS Options</summary>
```js
config.corsOrigins = ['*']
config.corsMethods = require('node:http').METHODS
config.corsHeaders = ['content-type', 'authorization']
config.corsCredentials = true
config.corsMaxAge = 0 // seconds to cache the preflight req
config.corsExposedHeaders = [] // headers you need to access in client-side JS
```
</details>
<br/>
### `onReady?: (dashboardUrl: string) => void`
By default, it will open the dashboard in your default browser on macOS and
Windows. But for a more cross-platform utility you could `npm install open` and
that implementation will be automatically used instead.
If you don’t want to open a browser, pass a noop:
```js
config.onReady = () => {}
```
At any rate, you can trigger any command besides opening a browser.
<br/>
### `logLevel?: 'quiet' | 'normal' | 'verbose'`
Defaults to `'normal'`.
- `quiet`: only errors (stderr)
- `normal`: info, mock access, warnings, and errors
- `verbose`: normal + API access
</details>
<details>
<summary>Programmatic Launch (Optional)…</summary>
```js
import { Mockaton } from 'mockaton'
import config from './mockaton.config.js'
const server = await Mockaton(
config // Not required, but it’s not read by default.
)
```
</details>
<br/>
## You can write JSON mocks in JavaScript or TypeScript
_TypeScript needs **Node 22.18+ or 23.6+**_
For example, `api/foo.GET.200.js`
### Option A: An Object, Array, or String is sent as JSON
```js
export default { foo: 'bar' }
```
### Option B: Function Mocks (async or sync)
**Return** a `string | Buffer | Uint8Array`, but **don’t call** `response.end()`
```js
export default (request, response) =>
JSON.stringify({ foo: 'bar' })
```
#### About Custom HTTP Handlers
For example, you can intercept requests to write to a database. Or
act based on some query string value, etc. In summary, you get Node’s
`request`, `response` as arguments, so you can think of Mockaton as a
router, but in the handlers you return, instead of ending the response.
<details>
<summary><b>Examples…</b></summary>
Imagine you have an initial list of colors, and
you want to concatenate newly added colors.
`api/colors.POST.201.js`
```js
import { parseJSON } from 'mockaton'
export default async function insertColor(request, response) {
const color = await parseJSON(request)
globalThis.newColorsDatabase ??= []
globalThis.newColorsDatabase.push(color)
// These two lines are not needed but you can change their values
// response.statusCode = 201 // default derived from filename
// response.setHeader('Content-Type', 'application/json') // unconditional default
return JSON.stringify({ msg: 'CREATED' })
}
```
`api/colors.GET.200.js`
```js
import colorsFixture from './colors.json' with { type: 'json' }
export default function listColors() {
return JSON.stringify([
...colorsFixture,
...(globalThis.newColorsDatabase || [])
])
}
```
</details>
<br/>
**What if I need to serve a static .js or .ts?**
**Option A:** Put it in your `config.staticDir` without the `.GET.200.js` extension.
Mocks in `staticDir` take precedence over `mocksDir/*`.
**Option B:** Read it and return it. For example:
```js
import { readFileSync } from 'node:fs'
export default function (_, response) {
response.setHeader('Content-Type', 'application/javascript')
return readFileSync('./some-dir/foo.js', 'utf8')
}
```
<br/>
## Mock Filename Convention
### Extension
The last three dots are reserved for the HTTP Method,
Response Status Code, and File Extension.
```
api/user.GET.200.json
```
You can also use `.empty` or `.unknown` if you don’t
want a `Content-Type` header in the response.
<details>
<summary>Supported Methods</summary>
<p>
ACL, BIND, CHECKOUT,
CONNECT, COPY, DELETE,
GET, HEAD, LINK,
LOCK, M-SEARCH, MERGE,
MKACTIVITY, MKCALENDAR, MKCOL,
MOVE, NOTIFY, OPTIONS,
PATCH, POST, PROPFIND,
PROPPATCH, PURGE, PUT,
QUERY, REBIND, REPORT,
SEARCH, SOURCE, SUBSCRIBE,
TRACE, UNBIND, UNLINK,
UNLOCK, UNSUBSCRIBE
</p>
</details>
<br/>
### Dynamic parameters
Anything within square brackets is always matched.
For example, for <a href="#">/api/company/<b>123</b>/user/<b>789</b></a>,
the filename could be:
<pre><code>api/company/<b>[id]</b>/user/<b>[uid]</b>.GET.200.json</code></pre>
<br/>
### Comments
Comments are anything within parentheses, including them.
They are ignored for routing purposes, so they have no effect
on the URL mask. For example, these two are for `/api/foo`
<pre>
api/foo<b>(my comment)</b>.GET.200.json
api/foo.GET.200.json
</pre>
A filename can have many comments.
### Default mock for a route
You can add the comment: `(default)`.
Otherwise, the first file in **alphabetical order** wins.
<pre>
api/user<b>(default)</b>.GET.200.json
</pre>
<br/>
### Query string params
The query string is ignored for routing purposes. It’s only used for
documenting the URL contract.
<pre>
api/video<b>?limit=[limit]</b>.GET.200.json
</pre>
On Windows, filenames containing "?" are [not
permitted](https://learn.microsoft.com/en-us/windows/win32/fileio/naming-a-file), but since that’s part of the query
string, it’s ignored anyway.
<br/>
### Index-like routes
If you have <a href="#">api/foo</a> and <a href="#">api/foo/bar</a>, you have two options:
**Option A.** Standard naming:
```
api/foo.GET.200.json
api/foo/bar.GET.200.json
```
**Option B.** Omit the URL on the filename:
```text
api/foo/.GET.200.json
api/foo/bar.GET.200.json
```
<br/>
<br/>
## Commander API
[openapi.yaml](docs/openapi.yaml)
`Commander` is a JavaScript client for Mockaton’s HTTP API.
All of its methods return their `fetch` response promise.
```js
import { Commander } from 'mockaton'
const myMockatonAddr = 'http://localhost:4040'
const mockaton = new Commander(myMockatonAddr)
```
<details>
<summary><b>See Commander Documentation</b></summary>
### Select a mock file for a route
```js
await mockaton.select('api/foo.200.GET.json')
```
### Toggle 500
- Selects the first found 500, which could be the autogenerated one.
- Or, selects the default file.
```js
await mockaton.toggle500('GET', '/api/foo')
```
### Select all mocks that have a particular comment
```js
await mockaton.bulkSelectByComment('(demo-a)')
```
Parentheses are optional, so you can pass a partial match. For example,
passing `'demo-'` (without the final `a`) works too. On routes
with many partial matches, their first mock in alphabetical order wins.
<br/>
### Set route is delayed flag
```js
await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
// or
await mockaton.setStaticRouteIsDelayed('/api/foo', true)
```
<br/>
### Set static route status
```js
await mockaton.setStaticRouteStatus('/api/foo', 404)
```
### Set route is proxied flag
```js
await mockaton.setRouteIsProxied('GET', '/api/foo', true)
```
<br/>
### Select a cookie
In `config.cookies`, each key is the label used for selecting it.
```js
await mockaton.selectCookie('My Normal User')
```
<br/>
### Set fallback proxy server address
```js
await mockaton.setProxyFallback('http://example.com')
```
Pass an empty string to disable it.
### Set save proxied responses as mocks flag
```js
await mockaton.setCollectProxied(true)
```
<br/>
### Set global delay value
```js
await mockaton.setGlobalDelsy(1200) // ms
```
<br/>
### Set CORS allowed
```js
await mockaton.setCorsAllowed(true)
```
<br/>
### Reset
Re-initialize the collection. The selected mocks, cookies, and delays go back to
default, but the `proxyFallback`, `collectProxied`, and `corsAllowed` are not affected.
```js
await mockaton.reset()
```
</details>
<br/>
## Alternatives worth learning as well
<details>
<summary>Learn more…</summary>
### Proxy-like
These are similar to Mockaton in the sense that you can modify the
mock response without loosing or risking your frontend code state. For
example, if you are polling, and you want to test the state change.
- Chrome DevTools allows for [overriding responses](https://developer.chrome.com/docs/devtools/overrides).
- Reverse Proxies such as [Burp](https://portswigger.net/burp) are also handy for overriding responses. Not easy but
very powerful.
### Client Side
In contrast to Mockaton, which is an HTTP Server, these
programs hijack your browser’s HTTP client (or Node’s).
- [Mock Server Worker (MSW)](https://mswjs.io)
- [Nock](https://github.com/nock/nock)
- [Fetch Mock](https://github.com/wheresrhys/fetch-mock)
- [Mentoss](https://github.com/humanwhocodes/mentoss)
### Server Side
- [Wire Mock](https://github.com/wiremock/wiremock)
- [Mock](https://github.com/dhuan/mock)
- [Swagger](https://swagger.io/)
- [Mockoon](https://mockoon.com)
</details>
<br/>
<br/>
