Skip to content

ericfortis/mockaton

Repository files navigation

Mockaton Logo

NPM Version NPM Version

An HTTP mock server for simulating APIs with minimal setup — ideal for triggering 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/user/1234 the filename would be:

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

Dashboard

On the dashboard you can select a mock variant for a particular route, delaying responses, or triggering an autogenerated 500 (Internal Server Error), among other features.

Nonetheless, there’s a programmatic API, which is handy for setting up tests (see Commander API section).

Mockaton Dashboard

Multiple Mock Variants

Each route can have different mocks. There are two options for doing that:

Adding comments to the filename

Comments are anything within parentheses, including them.

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

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.

api/videos(empty list).GET.204.json  # No Content
api/videos.GET.403.json              # Forbidden
api/videos.GET.500.txt               # Internal Server Error

Fallback to Your Backend

No need to mock everything. Mockaton can forward requests to your backend for routes you don’t have mocks for, or routes that have the ☁️ Cloud Checkbox checked.

Scraping mocks from your backend

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


Basic Usage

Mockaton is a Node.js program with no build or runtime NPM dependencies.

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

Demo App (Vite)

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

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

npm run mockaton
npm run start
# BTW, that directory has scripts for running Mockaton and Vite 
# with one command in two terminals.

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
  • Mocking third-party APIs
  • 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 check out long-lived branches, so you don’t have to downgrade backends to old API contracts or databases.

Simulating complex backend states

Sometimes, the ideal flow you need is too difficult to reproduce from your actual backend. For this, you can Bulk Select mocks by comments to simulate the complete states you want. For example, by adding (demo-part1), (demo-part2) to the filenames.

Similarly, you can deploy a Standalone Demo Server by compiling the frontend app and putting its built assets in config.staticDir. And simulate the flow by Bulk Selecting mocks.


Motivation

  • Avoids spinning up and updating hefty backends when developing UIs.
  • Allows 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 by waiting for backend APIs.

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 that allow you to 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 routing 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

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.

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

Query string params

The query string is ignored for routing purposes. In other words, it’s only used for documenting the URL contract.

api/video?limit=[limit].GET.200.json

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. Standard naming:

api/foo.GET.200.json
api/foo/bar.GET.200.json

Option B. Omit the URL on 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.

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, and they take precedence over corresponding 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 // Unreachable

ignore?: RegExp

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


host?: string

Defaults to 'localhost'

port?: number

Defaults to 0, which means auto-assigned


delay?: number

Defaults to 1200 milliseconds. Although routes can individually be delayed with the 🕓 Checkbox, the amount is globally configurable with this option.


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

formatCollectedJSON?: boolean

Defaults to true. Saves the mock with the formatting output of JSON.stringify(data, null, ' ') (two spaces indentation).


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 one-dimensional 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 Mockaton will use that implementation instead.

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 flag

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 server address

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

Pass an empty string to disable it.

Set save proxied responses as mocks flag

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

Alternatives worth learning as well

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.

Client side

In contrast to Mockaton, which is an HTTP Server, these programs mock the client (e.g., fetch) in Node.js and browsers.

About

HTTP Mock Sever

Topics

Resources

License

Stars

Watchers

Forks