Skip to content

ericfortis/mockaton

Repository files navigation

Mockaton Logo

NPM Version NPM Version

Mock your APIs, Enhance your Development Workflow

Mockaton is an HTTP mock server built 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, you don’t need to mock all your APIs. You can request from your backend the routes you don’t have mocks for. That’s done with:

config.proxyFallback = 'http://mybackend'

Scraping Mocks

You can save mocks following the filename convention for the routes that reached your proxyFallback with:

config.collectProxied = true

Multiple Mock Variants

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

Dashboard

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).

Mockaton Dashboard Demo

Basic Usage

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'), // must exist
  port: 2345
})
node --import=tsx my-mockaton.js

Running the Built-in Demo

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.

Use Cases

Testing

  • 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 resources

Time Travel

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.

Deterministic Standalone Demo Server

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, on the dashboard "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).

Motivation

  • 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.

Alternatives


You can write JSON mocks in JavaScript or TypeScript

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 || [])
  ])
}

What if I need to serve a static .js? Put it in your config.staticDir without the mock filename convention.


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

Dynamic Parameters

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

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

Default Mock for a Route

You can add the comment: (default). Otherwise, the first file in alphabetical order wins.

api/user(default).GET.200.json

Query String Params

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.

Index-like routes

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

Config

mocksDir: string

This is the only required field

host?: string

Defaults to 'localhost'

port?: number

Defaults to 0, which means auto-assigned

ignore?: RegExp

Defaults to /(\.DS_Store|~)$/

delay?: number

Defaults to config.delay=1200 milliseconds.

Although routes can individually be delayed with the 🕓 checkbox, delay the amount is globally configurable.

proxyFallback?: string

Lets you specify a target server for serving routes you don’t have mocks for. For example, config.proxyFallback = 'http://example.com'

collectProxied?: boolean

Defaults to false. With this flag you can save mocks that hit your proxy fallback to config.mocksDir. If there are UUIDv4 in the URL the filename will have [id] in their place. For example,

/api/user/d14e09c8-d970-4b07-be42-b2f4ee22f0a6/likes =>
my-mocks-dir/api/user/[id]/likes.GET.200.json

Your existing mocks won’t be overwritten (they don’t hit the fallback server).

Extension Details

An .empty extension means the Content-Type header was not sent by your backend.

An .unknown extension means the Content-Type is not in Mockaton’s predefined list. For that, you can add it to config.extraMimes

staticDir?: string

  • 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

cookies?: { [label: string]: string }

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 its payload.

extraHeaders?: string[]

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'
]

extraMimes?: { [fileExt: string]: string }

config.extraMimes = {
  jpe: 'application/jpeg'
}

These media types take precedence over the built-in utils/mime.js, so you can override them.

plugins?: [filenameTester: RegExp, plugin: Plugin][]

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, it fallbacks to reading the file from disk and computing the MIME from the extension.

Note: don’t call response.end() on any plugin.

See Plugin Examples
npm install yaml
import { parse } from 'yaml'
import { readFileSync } from 'node:js'
import { jsToJsonPlugin } from 'mockaton'

config.plugins = [
  
  // Although `jsToJsonPlugin` is set by default, you need to add it to your list if you need it.
  // In other words, your plugins array overwrites the default list. This way you can remove it.
  [/\.(js|ts)$/, jsToJsonPlugin], 
  
  [/\.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()
  }
}

corsAllowed?: boolean

Defaults to true. When 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

onReady?: (dashboardUrl: string) => void

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 API

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)

Select a mock file for a route

await mockaton.select('api/foo.200.GET.json')

Select all mocks that have a particular comment

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.

Set Route is Delayed Flag

await mockaton.setRouteIsDelayed('GET', '/api/foo', true)

Select a cookie

In config.cookies, each key is the label used for selecting it.

await mockaton.selectCookie('My Normal User')

Set Fallback Proxy

await mockaton.setProxyFallback('http://example.com')

Pass an empty string to disable it.

Reset

Re-initialize the collection. The selected mocks, cookies, and delays go back to default, but config.proxyFallback and config.corsAllowed are not affected.

await mockaton.reset()

“Use Mockaton”