Mockaton is a mock server for improving the frontend development and testing experience.
With Mockaton, you don’t need to write code for wiring your mocks. Instead, it
scans a given directory for filenames following a convention similar to the
URL paths. For example, the following file will be served on /api/user/1234
my-mocks-dir/api/user/[user-id].GET.200.json
By the way, this browser extension can create a TAR of your requests following that convention.
Nonetheless, you don’t need to mock all your APIs. Mockaton
can request from your backend the routes you don’t have mocks for.
That’s done with config.proxyFallback = 'http://mybackend'
Each route can have many mocks, which could either be:
- Different response status code. For example, for triggering errors.
- Comment on the filename, which is anything within parentheses.
- e.g.
api/login(locked out user).POST.423.json
- e.g.
In the dashboard, you can select a mock variant for a particular route, among other options. In addition, there’s a programmatic API, which is handy for setting up tests (see Commander API below).
tsx
is only needed if you want to write mocks in TypeScript
npm install mockaton tsx
Create a my-mockaton.js
file
import { resolve } from 'node:path'
import { Mockaton } from 'mockaton'
// See the Config section for more options
Mockaton({
mocksDir: resolve('my-mocks-dir'),
port: 2345
})
node --import=tsx my-mockaton.js
This demo uses the sample-mocks/ of this repository.
git clone https://github.com/ericfortis/mockaton.git
cd mockaton
npm install tsx
npm run demo:ts
Experiment with the Dashboard:
- Pick a mock variant from the Mock dropdown
- Toggle the 🕓 Delay Responses button, (e.g. for testing spinners)
- Toggle the 500 button, which sends and Internal Server Error on that endpoint
Finally, edit a mock file in your IDE. You don’t need to restart Mockaton for that. The Reset button is for registering newly added, removed, or renamed mocks.
- Empty responses
- Spinners by delaying responses
- Errors such as Bad Request and Internal Server Error
- Setting up UI tests
- Polled resources (for triggering their different states)
- alerts
- notifications
- slow to build assets
If you commit the mocks to your repo, it’s straightforward to bisect bugs and checking out long-lived branches. In other words, you don’t have to downgrade backends to old API contracts or databases.
Perhaps you need to demo your app, but the ideal flow is too complex to
simulate from the actual backend. In this case, compile your frontend app and
put its built assets in config.staticDir
. Then, from the Mockaton dashboard
you can "Bulk Select" mocks to simulate the complete states you want to demo.
For bulk-selecting, you just need to add a comment to the mock
filename, such as (demo-part1)
, (demo-part2)
.
- Avoids spinning up and maintaining hefty backends when developing UIs.
- For a deterministic, comprehensive, and consistent backend state. For example, having a collection with all the possible state variants helps for spotting inadvertent bugs.
- Sometimes, frontend progress is blocked waiting for some backend API. Similarly, it’s often delayed due to missing data or inconvenient contracts. Therefore, many meetings can be saved by prototyping frontend features with mocks, and then showing those contracts to the backend team.
- Chrome DevTools allows for overriding responses
- Reverse Proxies such as Burp are also handy for overriding responses.
- Mock Server Worker
For example, api/foo.GET.200.js
Option A: An Object, Array, or String is sent as JSON.
export default [{ foo: 'bar' }]
Option B: Function
Return a string | Buffer | Uint8Array
, but don’t call response.end()
export default (request, response) =>
JSON.stringify({ foo: 'bar' })
Think of these functions as HTTP handlers, so you can intercept requests. For example, for writing to a database.
See Intercepting Requests Examples
Imagine you have an initial list of colors, and you want to concatenate newly added colors.
api/colors.POST.201.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
import colorsFixture from './colors.json' with { type: 'json' }
export default function listColors() {
return JSON.stringify([
...colorsFixture,
...(globalThis.newColorsDatabase || [])
])
}
If you are wondering, what if I need to serve a static .js
?
Put it in your config.staticDir
without the mock filename convention.
The last 3 dots are reserved for the HTTP Method, Response Status Code, and the File Extension.
api/user.GET.200.json
Anything within square brackets is always matched. For example, for this route
/api/company/1234/user/5678
api/company/[id]/user/[uid].GET.200.json
Comments are anything within parentheses, including them.
They are ignored for URL purposes, so they have no effect
on the URL mask. For example, these two are for /api/foo
api/foo(my comment).GET.200.json api/foo.GET.200.json
You can add the comment: (default)
.
Otherwise, the first file in alphabetical order wins.
api/user(default).GET.200.json
The query string is ignored when routing to it. In other words, it’s only used for documenting the URL contract.
api/video?limit=[limit].GET.200.json
Speaking of which, on Windows filenames containing "?" are not permitted, but since that’s part of the query string, it’s ignored anyway.
If you have api/foo
and api/foo/bar
, you have two options:
Option A:
api/foo.GET.200.json
api/foo/bar.GET.200.json
Option B: Omit the filename.
api/foo/.GET.200.json
api/foo/bar.GET.200.json
This is the only required field
Defaults to 'localhost'
Defaults to 0
, which means auto-assigned
Defaults to /(\.DS_Store|~)$/
The response delay. It’s globally configurable, and it defaults to 1200
milliseconds.
Lets you specify a target server for serving routes you don’t have mocks for.
For example, config.proxyFallback = 'http://example.com'
- Use Case 1: If you have a bunch of static assets you don’t want to add
.GET.200.ext
- Use Case 2: For a standalone demo server. For example, build your frontend bundle, and serve it from Mockaton.
Files under config.staticDir
don’t use the filename convention.
They take precedence over the GET
mocks in config.mocksDir
.
For example, if you have two files for GET /foo/bar.jpg
my-static-dir/foo/bar.jpg
my-mocks-dir/foo/bar.jpg.GET.200.jpg // Unreacheable
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', {
email: 'john.doe@example.com',
picture: 'https://cdn.auth0.com/avatars/jd.png'
})
}
The selected cookie, which is the first one by default, is sent in every
response in a Set-Cookie
header. If you need to send more
cookies, inject them globally in config.extraHeaders
.
By the way, the jwtCookie
helper has a hardcoded header and signature.
In other words, it’s useful only if you care about the payload.
Note it’s a unidimensional array. The header name goes at even indices.
config.extraHeaders = [
'Server', 'Mockaton',
'Set-Cookie', 'foo=FOO;Path=/;SameSite=strict',
'Set-Cookie', 'bar=BAR;Path=/;SameSite=strict'
]
config.extraMimes = {
jpe: 'application/jpeg'
}
These media types take precedence over the built-in utils/mime.js, so you can override them.
type Plugin = (
filePath: string,
request: IncomingMessage,
response: OutgoingMessage
) => Promise<{
mime: string,
body: string | Uint8Array
}>
Plugins are for processing mocks before sending them.
Note: don’t call response.end()
on them.
See Plugin Examples
npm install yaml
import { parse } from 'yaml'
import { readFileSync } from 'node:js'
import { jsToJsonPlugin } from 'mockaton'
config.plugins = [
[/\.(js|ts)$/, jsToJsonPlugin], // Default but you need to add it to your list if you need it
[/\.yml$/, yamlToJsonPlugin],
[/foo\.GET\.200\.txt$/, capitalizePlugin], // e.g. GET /api/foo would be capitalized
]
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()
}
}
Defaults to corsAllowed = false
. When config.corsAllowed === true
, these are the default options:
config.corsOrigins = ['*']
config.corsMethods = ['GET', 'PUT', 'DELETE', 'POST', 'PATCH', 'HEAD', 'OPTIONS', 'TRACE', 'CONNECT']
config.corsHeaders = ['content-type']
config.corsCredentials = true
config.corsMaxAge = 0 // seconds to cache the preflight req
config.corsExposedHeaders = [] // headers you need to access in client-side JS
This defaults to trying to open the dashboard in your default browser on macOS and
Windows. For a more cross-platform utility, you could npm install open
and pass it.
import open from 'open'
config.onReady = open
If you don’t want to open a browser, pass a noop:
config.onReady = () => {}
Commander
is a wrapper for the Mockaton HTTP API.
All of its methods return their fetch
response promise.
import { Commander } from 'mockaton'
const myMockatonAddr = 'http://localhost:2345'
const mockaton = new Commander(myMockatonAddr)
await mockaton.select('api/foo.200.GET.json')
await mockaton.bulkSelectByComment('(demo-a)')
Parentheses are optional, so you can pass a partial match.
For example, passing 'demo-'
(without the final a
), selects the
first mock in alphabetical order that matches.
await mockaton.setRouteIsDelayed('GET', '/api/foo', true)
In config.cookies
, each key is the label used for selecting it.
await mockaton.selectCookie('My Normal User')
await mockaton.setProxyFallback('http://example.com')
Pass an empty string to disable it.
Re-initialize the collection. So if you added or removed mocks they will
be considered. The selected mocks, cookies, and delays go back to default,
but config.proxyFallback
and config.corsAllowed
are not affected.
await mockaton.reset()
- Refactor Tests