Skip to content

ericfortis/mockaton

Repository files navigation

Mockaton Logo

NPM Version NPM Version

Mockaton is an HTTP mock server with the goal of making your frontend development and testing easier—and a lot more fun.

With Mockaton you don’t need to write code for wiring your mocks. Instead, just place your mocks in a directory and it will be scanned for filenames following a convention similar to the URLs.

For example, for /api/user/1234 the mock filename would be:

my-mocks-dir/api/user/[user-id].GET.200.json

Dashboard

In the dashboard you can select a mock variant for a particular route, among other features such as delaying responses, or triggering an autogenerated 500 (Internal Server Error). Nonetheless, there’s a programmatic API, which is handy for setting up tests (see Commander API below).

Mockaton Dashboard

Fallback to your Backend

Mockaton can fallback to your real backend on routes you don’t have mocks for. For that, type your backend address in the Fallback Backend field.

Similarly, if you already have mocks for a route you can check the ☁️ Cloud checkbox and that route will be requested from your backend.

Scrapping Mocks from you Backend

If you check Save Mocks, Mockaton will collect the responses that hit your backend. Those mocks will be saved to your config.mocksDir following the filename convention.

Multiple Mock Variants

Adding comments in filenames

Want to mock a locked-out user or an invalid login attempt? Add a comment to the filename in parentheses. For example:

api/login(locked out user).POST.423.json

Different response status code

For instance, you can have mocks with a 4xx or 5xx status code for triggering error responses. Or with a 204 (No Content) for testing empty collections.

api/videos.GET.401.json
api/videos.GET.500.empty

Basic Usage

tsx is only needed if you want to write mocks in TypeScript.

npm install mockaton tsx --save-dev

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 demo app (Vite)

This is a minimal React + Vite + Mockaton app. It’s mainly a list of colors, which contains all of their possible states. For example, permutations for out-of-stock, new-arrival, and discontinued.

Also, if you select the Admin User from the Mockaton dashboard, the color cards will have a Delete button.

git clone https://github.com/ericfortis/mockaton.git
cd mockaton/demo-app-vite
npm install 
npm run mockaton
npm run start

By the way, that directory has scripts for opening Mockaton and Vite in one command.

The app looks like this:

Mockaton Demo App Screenshot

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
  • Mocking third-party APIs

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(assorted)(default).GET.200.ts

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

You can also use .empty or .unknown if you don’t want a Content-Type header in the response.

Supported Methods

From require('node:http').METHODS

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

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. The directory must exist.

host?: string

Defaults to 'localhost'

port?: number

Defaults to 0, which means auto-assigned

ignore?: RegExp

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

delay?: number

Defaults to 1200 milliseconds.

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

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 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. That is, the routes you manually selected for using your backend with the ☁️ Cloud Checkbox, will have a unique filename comment.

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', {
    name: 'John Doe',
    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, you can either inject them globally in config.extraHeaders, or in function .js or .ts mock.

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

Those extra 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, 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.

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 = require('node:http').METHODS
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

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 pass it.

import open from 'open'
config.onReady = open

If you don’t want to open a browser, pass a noop:

config.onReady = () => {}

At any rate, you can trigger any command besides opening a browser.


Commander API

Commander is a client for Mockaton’s 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)

Set Route is Proxied

await mockaton.setRouteIsProxied('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.

Set Save Proxied Mocks

await mockaton.setCollectProxied(true)

Reset

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

await mockaton.reset()