{"id":23050530,"url":"https://github.com/acrontum/moxy","last_synced_at":"2025-07-31T08:32:48.447Z","repository":{"id":39967825,"uuid":"481184990","full_name":"acrontum/moxy","owner":"acrontum","description":"Simple, configurable part mock part proxy","archived":false,"fork":false,"pushed_at":"2025-02-04T13:51:43.000Z","size":673,"stargazers_count":8,"open_issues_count":0,"forks_count":0,"subscribers_count":3,"default_branch":"main","last_synced_at":"2025-06-30T07:04:28.319Z","etag":null,"topics":["internal-development"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/acrontum.png","metadata":{"files":{"readme":"readme.md","changelog":"changelog.md","contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2022-04-13T11:17:17.000Z","updated_at":"2025-02-04T13:49:09.000Z","dependencies_parsed_at":"2024-01-28T13:00:13.224Z","dependency_job_id":"2e6224f2-518a-4ddf-a660-9e8ed5701a93","html_url":"https://github.com/acrontum/moxy","commit_stats":{"total_commits":87,"total_committers":3,"mean_commits":29.0,"dds":0.04597701149425293,"last_synced_commit":"4b6aeae4cb773a0b1b9540c3ccd28efd1f57188a"},"previous_names":[],"tags_count":24,"template":false,"template_full_name":null,"purl":"pkg:github/acrontum/moxy","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acrontum%2Fmoxy","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acrontum%2Fmoxy/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acrontum%2Fmoxy/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acrontum%2Fmoxy/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/acrontum","download_url":"https://codeload.github.com/acrontum/moxy/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/acrontum%2Fmoxy/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":267874504,"owners_count":24158764,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","status":"online","status_checked_at":"2025-07-30T02:00:09.044Z","response_time":70,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["internal-development"],"created_at":"2024-12-15T23:33:24.578Z","updated_at":"2025-07-31T08:32:48.420Z","avatar_url":"https://github.com/acrontum.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Moxy\n\n\nSimple, configurable mock / proxy server with 0 dependencies.\n\n\u003cp align=\"center\"\u003e\n  \u003ca href=\"https://www.npmjs.org/package/@acrontum/moxy\" alt=\"npm version @acrontum/moxy\"\u003e\n    \u003cimg alt=\"npm version @acrontum/moxy\" src=\"https://img.shields.io/npm/v/@acrontum/moxy\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://hub.docker.com/r/acrontum/moxy/tags\" alt=\"Dockerhub image version acrontum/moxy\"\u003e\n    \u003cimg alt=\"Dockerhub image version acrontum/moxy\" src=\"https://img.shields.io/docker/v/acrontum/moxy?label=dockerhub\"\u003e\n  \u003c/a\u003e\n\n  \u003ca href=\"https://github.com/acrontum/moxy\" alt=\"Github latest tag acrontum/moxy\"\u003e\n    \u003cimg alt=\"Github latest tag acrontum/moxy\" src=\"https://img.shields.io/github/v/tag/acrontum/moxy\"\u003e\n  \u003c/a\u003e\n\n  \u003cimg alt=\"npm minified bundle size\" src=\"https://img.shields.io/bundlephobia/min/@acrontum/moxy\"\u003e\n\n  \u003cbr /\u003e\n\n  \u003ca href=\"https://github.com/acrontum/moxy/actions/workflows/build-node.yml\" alt=\"CI status (node workflow)\"\u003e\n    \u003cimg alt=\"CI status (node workflow)\" src=\"https://github.com/acrontum/moxy/actions/workflows/build-node.yml/badge.svg\"\u003e\n  \u003c/a\u003e\n  \u003ca href=\"https://github.com/acrontum/moxy/actions/workflows/build-docker.yml\" alt=\"CI status (docker workflow)\"\u003e\n    \u003cimg alt=\"CI status (docker workflow)\" src=\"https://github.com/acrontum/moxy/actions/workflows/build-docker.yml/badge.svg\"\u003e\n  \u003c/a\u003e\n\u003c/p\u003e\n\n---\n\n\u003c!--\nto regen:\n  npx doctoc --github readme.md\n\nthen manually remove %5C from the routes\n--\u003e\n\n\u003c!-- START doctoc generated TOC please keep comment here to allow auto update --\u003e\n\u003c!-- DON'T EDIT THIS SECTION, INSTEAD RE-RUN doctoc TO UPDATE --\u003e\n**Table of Contents**\n\n- [Quick Start](#quick-start)\n  - [Installation](#installation)\n  - [Programatic](#programatic)\n  - [CLI](#cli)\n  - [Docker](#docker)\n  - [Docker compose](#docker-compose)\n- [Examples (see the examples folder)](#examples-see-the-examples-folder)\n  - [Simple server](#simple-server)\n  - [Common config options](#common-config-options)\n  - [Use it in tests](#use-it-in-tests)\n  - [Using variable replacement](#using-variable-replacement)\n  - [Configure a basic file server](#configure-a-basic-file-server)\n  - [Use as a proxy](#use-as-a-proxy)\n  - [Serve a local folder as a git repo for cloning](#serve-a-local-folder-as-a-git-repo-for-cloning)\n  - [More](#more)\n- [Configuration](#configuration)\n  - [Via HTTP requests](#via-http-requests)\n  - [From files](#from-files)\n- [API](#api)\n  - [CLI options](#cli-options)\n  - [HTTP API](#http-api)\n    - [GET /\\_moxy:](#get-_moxy)\n    - [GET /\\_moxy/routes](#get-_moxyroutes)\n    - [GET /\\_moxy/router](#get-_moxyrouter)\n    - [POST /\\_moxy/router](#post-_moxyrouter)\n    - [PATCH /\\_moxy/router/:route](#patch-_moxyrouterroute)\n    - [PUT /\\_moxy/router/:route](#put-_moxyrouterroute)\n    - [DELETE /\\_moxy/router/:route](#delete-_moxyrouterroute)\n\n\u003c!-- END doctoc generated TOC please keep comment here to allow auto update --\u003e\n\n\n## Quick Start\n\n\n### Installation\n\n```bash\nnpm i -D @acrontum/moxy\n```\n\n\n### Programatic\n\n\n```typescript\nimport { MoxyServer } from '@acrontum/moxy';\n\nconst moxy = new MoxyServer();\n\nmoxy.on('hello/world', {\n  get: {\n    status: 200,\n    body: {\n      message: 'Welcome!'\n    }\n  }\n});\n\nawait moxy.listen(5000);\n```\n\n\n### CLI\n\n```bash\n# load from local routes folder, and add single get handler for /hello/world\nnpx @acrontum/moxy --port 5000 --routes ./routes/ --on '{\n  \"path\": \"hello/world\",\n  \"config\": {\n    \"get\": {\n      \"status\": 418,\n      \"body\": {\n        \"message\": \"I am a teapot\"\n      }\n    }\n  }\n}'\n```\n\n\n### Docker\n\n```bash\ndocker run \\\n  --name moxy \\\n  --publish 5000:5000 \\\n  --volume $PWD/routes:/opt/routes \\\n   acrontum/moxy --port 5000 --routes /opt/routes\n\n# OR acrontum/moxy --port 5000 --on '{ \"path\": \"hello/world\", \"config\": { \"get\": { \"status\":200, \"body\":{ \"message\":\"Welcome!\" } } } }'\n```\n\n\n### Docker compose\n\n```yaml\nversion: \"3.7\"\n\nservices:\n  moxy:\n    image: acrontum/moxy\n    container_name: moxy\n    volumes:\n      - ./routes:/opt/routes\n    environment:\n      - PORT=80\n    ports:\n      - 5000:80\n    init: true\n    command: --allow-http-config --routes /opt/routes\n```\n\n\n## Examples (see [the examples folder](./examples))\n\n**note** that examples are in TypeScript for clarity, but moxy is a javascript library so you will need to transpile if you want to use TypeScript.  \n\n\n### Simple server\n\n```typescript\nimport { MoxyServer } from '@acrontum/moxy';\n\nconst moxy = new MoxyServer();\nawait moxy.listen(5000);\n\nmoxy.on('/api/cats', {\n  get: {\n    body: [\n      { name: 'alice', flavour: 'yellow' },\n      { name: 'bob', flavour: 'black' },\n      { name: 'cheshire', flavour: 'stripey' }\n    ]\n  }\n});\n// curl localhost:5000/api/cats\n//   -\u003e 200 [{\"name\":\"alice\",\"flavour\":\"yellow\"},{\"name\":\"bob\",\"flavour\":\"black\"},{\"name\":\"cheshire\",\"flavour\":\"stripey\"}]\n\nmoxy.on('/api/cats/alice', {\n  get: {\n    body: { \n      name: 'alice', \n      flavour: 'yellow'\n    }\n  }\n});\n// curl localhost:5000/api/cats/alice\n//   -\u003e 200 {\"name\":\"alice\",\"flavour\":\"yellow\"}\n```\n\n\n### Common config options\n\n```typescript\nimport { MoxyServer } from '@acrontum/moxy';\n\n// initialize with default settings (optional)\nconst moxy = new MoxyServer({\n  logging: 'verbose',\n  router: {\n    allowHttpRouteConfig: false\n  }\n});\n\n// start listening on port 5000 (can be done before or after route configuration)\nawait moxy.listen(5000);\n\n// configure listener for a GET /some/path, responding with a 200 and text body \"Hi!\"\nmoxy.on('/some/path', {\n  get: {\n    status: 200,\n    body: 'Hi!',\n    headers: { 'Content-Type': 'text/plain' }\n  }\n});\n\n// configure multiple routes (/auth/login, /auth/logout, and /auth/register) by prefix:\nmoxy.onAll('/auth/', {\n  '/login/': {\n    post: {\n      status: 401,\n      body: { message: 'Unauthorized' }\n    }\n  },\n  '/logout/': {\n    post: {\n      status: 204\n    }\n  },\n  '/register/': {\n    post: {\n      status: 201,\n      body: { message: 'welcome' }\n    }\n  }\n});\n\n// handle all HTTP verbs with a request handler\nmoxy.on('/not/a/test', (req: MoxyRequest, res: MoxyResponse, variables: HandlerVariables) =\u003e {\n  console.log('Hi world.');\n\n  return res.sendJson({ pewPew: 'lazors' });\n});\n\nmoxy.on('/still/not/a/test', {\n  patch: (req: MoxyRequest, res: MoxyResponse, variables: HandlerVariables) =\u003e {\n    console.log('Updating the world.');\n\n    return res.sendJson({ pewPew: 'lazors' });\n  }\n});\n\n// load from filesystem\nawait moxy.addRoutesFromFolder('/path/to/my/routes');\n\n// using basic variable replacement, search in a static folder and return an image\nmoxy.on('/users/:userId/profile-picture', '/static/images/:userId.png');\n\n// ... make requests against moxy\n\n// stop the server. closeConnections: true will close any active connections immediately instead of waiting for them.\nawait moxy.close({ closeConnections: true });\n```\n\n\n### Use it in tests\n\n```typescript\nimport { expect } from 'chai';\nimport { after, afterEach, before } from 'mocha';\nimport { default as supertest } from 'supertest';\nimport { MoxyServer } from '@acrontum/moxy';\nimport { MyApplication } from '../path/to/my/app/src';\n\ndescribe('API auth', () =\u003e {\n  const moxy: MoxyServer = new MoxyServer({ logging: 'error' });\n  let request: supertest.SuperTest\u003csupertest.Test\u003e;\n\n  before(async () =\u003e {\n    moxy.on('/auth/login', {\n      post: {\n        status: 200,\n        body: {\n          username: 'bob'\n        },\n        headers: {\n          'Set-Cookie': 'om-nom-nom;'\n        }\n      }\n    });\n    await moxy.listen(5001);\n\n    MyApplication.configure({ authUrl: 'http://localhost:5001' });\n    await MyApplication.start();\n\n    request = supertest(MyApplication.expressApp);\n  });\n\n  after(async () =\u003e {\n    await moxy.close({ closeConnections: true });\n    await MyApplication.stop();\n  });\n\n  it('can authenticate users', async () =\u003e {\n    await request.post('/v1/login').send({ username: 'bob', password: 'yes' })  \n      .expect(({ status, body }) =\u003e {\n        expect(status).equals(200);\n        expect(body).deep.equals({ username: 'bob' });\n      });\n\n    // no more spyOn(http.send)!\n    // test your full E2E http request flow\n    moxy.once('/auth/login', {\n      post: {\n        status: 401,\n        body: {\n          message: 'Unknown user or invalid password'\n        }\n      }\n    });\n\n    await request.post('/v1/login').send({ username: 'bob', password: 'yes' })  \n      .expect(({ status, body }) =\u003e {\n        expect(status).equals(401);\n        expect(body).equals('Failed to login');\n      });\n  });\n});\n```\n\n\n### Using variable replacement\n\n```typescript\nimport { MoxyServer, MoxyRequest, MoxyResponse, HandlerVariables } from '@acrontum/moxy';\n\nconst moxy = new MoxyServer();\nawait moxy.listen(5000);\n\nmoxy.on('/echo/:name', {\n  get: {\n    status: 200,\n    body: {\n      hello: ':name'\n    },\n  }\n});\n// curl localhost:5000/echo/bob\n//   -\u003e 200 {\"hello\":\"bob\"}\n// curl localhost:5000/echo/bob/and/jane\n//   -\u003e 404\n\nmoxy.on('/echo-with-slash/(?\u003cpathWithSlash\u003e.+)', {\n  get: {\n    status: 200,\n    body: {\n      hello: ':pathWithSlash'\n    },\n  }\n});\n// curl localhost:5000/echo-with-slash/this/will/be/in/the/body\n//   -\u003e 200 {\"hello\":\"this/will/be/in/the/body\"}\n\nmoxy.on('/query\\\\?search=:querySearch', {\n  get: {\n    status: 200,\n    body: {\n      youSearchedFor: ':querySearch'\n    },\n  }\n});\n\nmoxy.on('/query(.*)', {\n  get: (req: MoxyRequest, res: MoxyResponse, vars: HandlerVariables) =\u003e {\n    return res.sendJson({ status: 418, query: vars });\n  }\n});\n// curl localhost:5000/query\n//   -\u003e {\"status\":418,\"query\":{}}\n\n// curl 'localhost:5000/query?search=hello'\n//   -\u003e {\"youSearchedFor\":\"hello\"}\n\n// curl 'localhost:5000/query?test=hello'\n//   -\u003e {\"status\":418,\"query\":{\"test\":\"hello\"}}\n```\n\n\n### Configure a basic file server\n\n```typescript\nimport { MoxyServer, MoxyRequest, MoxyResponse, HandlerVariables } from '@acrontum/moxy';\nimport { createWriteStream, promises } from 'fs';\nimport { join, dirname, resolve } from 'path';\n\nconst moxy = new MoxyServer();\n\n// simple file server (GET only, no nested paths)\n\n// NOTE moxy will not accept paths outside of the target assets folder by default\n// such as /home/file.png or ../../file.png. To disable this behaviour, use a\n// request handler\nmoxy.on('/v1/assets/:filename', './assets/:filename');\n\n// file server with upload\nmoxy.on('/v1/database/(?\u003cfilename\u003e.+)', {\n  get: './disk-db/:filename',\n  put: async (req: MoxyRequest, res: MoxyResponse, vars: HandlerVariables) =\u003e {\n    if (/\\.\\./.test(vars.filename)) {\n      return res.sendJson({ status: 422, message: 'Invalid filename' });\n    }\n\n    const outfile = join('./disk-db', vars.filename);\n    await promises.mkdir(resolve(dirname(outfile)), { recursive: true });\n\n    const output = createWriteStream(outfile, 'utf-8');\n    req.on('end', () =\u003e res.sendJson({ status: 201, message: 'ok' }));\n\n    req.pipe(output);\n  }\n});\n\nawait moxy.listen(5000);\n\n/*\nmkdir assets/\necho 'hello world!' \u003e assets/welcome.txt\n\ncurl localhost:5000/v1/assets/hello.html\n#  -\u003e 404\n\ncurl localhost:5000/v1/assets/welcome.txt\n#  -\u003e 200 hello world!\n\ncurl localhost:5000/v1/database/secrets/passwords.txt\n#  -\u003e 404\n\ncurl -XPUT localhost:5000/v1/database/secrets/passwords.txt -d 'admin=super-secret'\n#  -\u003e 201\n\ncurl localhost:5000/v1/database/secrets/passwords.txt\n#  -\u003e 200 admin=super-secret\n*/\n```\n\n\n### Use as a proxy\n\n```typescript\nimport { MoxyServer } from '@acrontum/moxy';\n\nconst moxy = new MoxyServer({ router: { allowHttpRouteConfig: true } });\n\n// transparent proxy moxy -\u003e google\nmoxy.on('/(?\u003cpath\u003e.*)', {\n  proxy: 'https://www.google.ca/:path'\n});\n\nawait moxy.listen(5000);\n\n/*\n\nopen http://localhost:5000/search?q=acrontum/moxy\n\nsince we allowed httpConfig, we can do this from the browser devtools:\n\nfetch('http://localhost:5000/_moxy/routes', {\n  method: 'POST',\n  headers: { 'Content-Type': 'application/json' },\n  body: JSON.stringify({\n    path: '/images/branding/googlelogo/2x/googlelogo_color_92x30dp.png',\n    config: {\n      proxy: 'https://upload.wikimedia.org/wikipedia/commons/thumb/d/d9/Flag_of_Canada_%28Pantone%29.svg/255px-Flag_of_Canada_%28Pantone%29.svg.png'\n    }\n  })\n});\n\nand now when we refresh, the google logo is a Canada flag (much better, eh?)\n*/\n```\n\nOr a 1-liner from the cli to spin up a quick proxy server\n\n```bash\nnpx @acrontum/moxy --port 5000 --on '{ \"path\": \"/(?\u003cpath\u003e.*)\", \"config\": { \"proxy\": \"http://corporate.ca/:path\" } }' --allow-http-config\n```\n\n\n### Serve a local folder as a git repo for cloning\n\n```typescript\nimport { HandlerVariables, MoxyRequest, MoxyResponse, MoxyServer } from '@acrontum/moxy';\nimport { exec, spawn } from 'child_process';\nimport { existsSync } from 'fs';\nimport { rm } from 'fs/promises';\nimport { join } from 'path';\nimport { promisify } from 'util';\n\nconst moxy = new MoxyServer();\n\nconst run = promisify(exec);\n\nconst pack = (service: string) =\u003e {\n  const name = `# service=${service}\\n`;\n  const len = (4 + name.length).toString(16);\n\n  return `${Array(4 - len.length + 1).join('0')}${len}${name}0000`;\n};\n\nconst getRepo = (repoName: string) =\u003e join('./repos', repoName.replace('.git', ''));\n\nmoxy.on('/projects/:repo/info/refs\\\\?service=:service', {\n  get: async (req: MoxyRequest, res: MoxyResponse, vars: HandlerVariables): Promise\u003cMoxyResponse\u003e =\u003e {\n    const repo = getRepo(vars.repo as string);\n    const service = vars.service as string;\n\n    if (!existsSync(repo)) {\n      return res.sendJson({ status: 404, body: 'Not found', vars });\n    }\n\n    try {\n      if (!existsSync(join(repo, '.git'))) {\n        await run(`git init -q \u0026\u0026 git add -A \u0026\u0026 git commit -am init`, { cwd: repo });\n      } else {\n        await run(`git add -A \u0026\u0026 git commit -am init || echo 'up to date'`, { cwd: repo });\n      }\n\n      res.writeHead(200, {\n        'Content-Type': `application/x-${service}-advertisement`,\n        'Cache-Control': 'no-cache',\n      });\n      res.write(pack(service));\n\n      const uploadPack = spawn(service, ['--stateless-rpc', '--advertise-refs', repo]);\n\n      return uploadPack.stdout.pipe(res);\n    } catch (e) {\n      console.error(e);\n    }\n\n    return res.sendJson({ status: 404, body: 'Not found', vars });\n  },\n});\n\nmoxy.on('/projects/:repo/git-upload-pack', {\n  post: async (req: MoxyRequest, res: MoxyResponse, vars: HandlerVariables): Promise\u003cMoxyResponse\u003e =\u003e {\n    const repo = getRepo(vars.repo as string);\n\n    if (!existsSync(repo)) {\n      return res.sendJson({ status: 404, body: 'Not found', vars });\n    }\n\n    try {\n      res.writeHead(200, {\n        'Content-Type': 'application/x-git-upload-pack-response',\n        'Cache-Control': 'no-cache',\n      });\n\n      const proc = spawn(`git-upload-pack`, ['--stateless-rpc', repo]);\n      proc.stdout.on('end', () =\u003e rm(join(repo, '.git'), { recursive: true }).catch(console.error));\n\n      req.pipe(proc.stdin);\n\n      return proc.stdout.pipe(res);\n    } catch (e) {\n      console.error(e);\n    }\n\n    return res.sendJson({ status: 404, body: 'Not found', vars });\n  },\n});\n\nawait moxy.listen(5000);\n\n/*\ngit clone http://localhost:5000/projects/server.git\ngit clone http://localhost:5000/projects/app\n*/\n```\n\n\n### More\n\nThe [examples folder](./examples/) contains most of the examples above, as well as the [example config](./examples/example-routing/example.routes.ts) which has common configuration options with comments:\n\n```typescript\nimport { HandlerVariables, MoxyRequest, MoxyResponse, Routes } from '@acrontum/moxy';\n\n// The export name is unused, so it can be anything. Moxy will use the path to the file and the values of the Routes to\n// configure the routing.\nexport const routeConfig: Routes = {\n  // example using basic path params and replacements.\n  '/:machineId/measurements/:measurementId': {\n    get: {\n      status: 200,\n      // variables from the path can be injected into the response.\n      // The simple version is \":variableName\" in the path params and body. By\n      // default, this will only match word boundaries (eg /:variable/).\n      body: `\u003c!DOCTYPE html\u003e\u003chtml\u003e\n        \u003chead\u003e\n          \u003cmeta charset=\"utf-8\"\u003e\n          \u003ctitle\u003e:machineId/:measurementId\u003c/title\u003e\n        \u003c/head\u003e\n        \u003cbody\u003e\n          \u003ch1\u003eMachine: :machineId - measurement: :measurementId\u003c/h1\u003e\n        \u003c/body\u003e\n      \u003c/html\u003e`,\n      headers: {\n        'Content-Type': 'text/html',\n      },\n    },\n  },\n  // The more complicated method is using regex capture groups. This allows for more control over how groups are\n  // captured (eg for matching slashes in the path).\n  '/static/(?\u003cfile\u003e.*)': {\n    // When the value for a method is a simple string, a file is assumed.\n    get: '/public/:file',\n  },\n  // This is the short form of the above:\n  '/assets/(?\u003cfile\u003e.*)': '/www-data/:file',\n  // With the above 2 configs, moxy will look in the `./public` folder for requests to `/static/path/to/file`, and the\n  // `./www-data` folder for requests to `/assets/path/to/file`. These are relative to the process's current directory,\n  // so if you ran from this folder it would look in `./images` and `./public` and try to return the file.\n  'auth/login': {\n    post: {\n      status: 200,\n      body: {\n        active: true,\n        user_id: 'user_id',\n      },\n    },\n  },\n  '/users/:username': {\n    patch: {\n      status: 200,\n      body: {\n        firstName: 'pat',\n        username: ':username',\n      },\n    },\n    get: {\n      status: 200,\n      body: {\n        firstName: 'pat',\n        username: ':username',\n      },\n    },\n  },\n  // example which proxies requests from moxy.test/proxied-server/\u003ctarget\u003e to google/\u003ctarget\u003e\n  'proxied-server(?\u003cpath\u003e.*)': {\n    // here, everything after proxied-server is passed through to the proxy target\n    proxy: 'https://www.google.com:path',\n    // proxy options are the same as http request options, and are passed through.\n    proxyOptions: {\n      headers: {\n        'x-auth-token': 'totally-real',\n      },\n    },\n  },\n  'manual-override': (request: MoxyRequest, response: MoxyResponse, variables: HandlerVariables) =\u003e {\n    response.writeHead(418);\n    response.end('I am a teapot');\n  },\n  'partly-manual-override/:userId': {\n    get: {\n      status: 418,\n      body: 'I am a teapot',\n    },\n    post: (request: MoxyRequest, response: MoxyResponse, variables: HandlerVariables) =\u003e {\n      response.writeHead(201);\n      response.end(`Brew started for ${variables.userId}`);\n    },\n  },\n  '/glacial/': {\n    // slow all methods by 100ms\n    delay: 100,\n    get: {\n      // slow only 1 method by 100ms\n      delay: 100,\n      status: 204,\n    },\n    delete: {\n      status: 204,\n    },\n  },\n  // note that by default, the path is converted to a regex, so special chars\n  // should be escaped\n  '/path/with/query\\\\?search=:theSearchThing': {\n    get: {\n      body: 'you searched for :theSearchThing',\n      headers: { 'Content-Type': 'text/plain' },\n    },\n  },\n  // passing exact: true will prevent the path from being converted to a regex.\n  // NOTE: this will also disable simple or regex replacements. Parsed query params will still be returned in\n  // HandlerVariables if you use a request handler (see below).\n  '/exact/match/:notCaptured?queryMustHave': {\n    exact: true,\n    get: {\n      status: 204,\n    },\n  },\n  // if the handler would normally be a function and exact matching is desired,the 'all' method can be used to achieve\n  // this.\n  '/exact/match/handler?ignore=(.*)': {\n    exact: true,\n    all: (request: MoxyRequest, response: MoxyResponse, variables: HandlerVariables) =\u003e {\n      return response.sendJson({ matchedExactly: true });\n    },\n  },\n};\n```\n\nSee [API](#api) for full usage.\n\n\n## Configuration\n\n\n### Via HTTP requests\n\nAssuming moxy is running at localhost:5000, and has [HTTP config](https://acrontum.github.io/moxy/interfaces/ServerConfig.html#router) enabled:\n\n```bash\n# hit the moxy api\ncurl localhost:5000/_moxy/\n\n# add a new handler that responds to GET /test/path with a 200 and text/plain \"neat\" response\ncurl localhost:5000/_moxy/routes \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"path\": \"/test/path\",\n    \"config\": {\n      \"get\": {\n        \"status\": 200,\n        \"body\": \"neat\"\n      }\n    }\n  }'\n\n# will show the array of paths avaiable\ncurl localhost:5000/_moxy/routes\n\n# will show paths and their handler object currently configured\ncurl localhost:5000/_moxy/router\n\n# test our new route\ncurl localhost:5000/test/path\n\n# remove our new route\ncurl -X DELETE localhost:5000/_moxy/routes/test/path\n\n# add a \"once\" route handler\ncurl localhost:5000/_moxy/routes?once=true \\\n  -H 'Content-Type: application/json' \\\n  -d '{\n    \"path\": \"/pew/pew\",\n    \"config\": {\n      \"get\": {\n        \"status\": 200,\n        \"body\": \"neat\"\n      }\n    }\n  }'\n\n# 200\ncurl localhost:5000/pew/pew\n\n# 404\ncurl localhost:5000/pew/pew\n```\n\nNote that you will not be able to configure the response using a function via HTTP.\n\nSee [API](#api) for full usage.\n\n\n### From files\n\nMoxy can load routing configs from the filesystem, searching recursively for `.js` or `.json` files matching `\u003canything\u003e.routes.js(on)`. This allows you to organize routes into files, and put them in a routes folder (as shown in the [example-routing folder](./examples/example-routing/)).\n\n```typescript\nawait moxy.addRoutesFromFolder('/path/to/routes/folder');\n```\n\n```bash\n# npx cli\nnpx @acrontum/moxy --routes /path/to/routes/folder\n\n# docker run\ndocker run acrontum/moxy --routes /path/to/routes/folder\n```\n\n```yaml\nversion: \"3.7\"\n\nservices:\n  moxy:\n    image: acrontum/moxy\n    volumes:\n      - ./routes:/opt/routes\n    command: --routes /opt/routes\n```\n\nWhen loading routes from folders, the tree structure will determine the routing. For instance, if your folder structure looks like this:\n```\npublic/\n├── routes\n│   ├── a\n│   │   ├── a.routes.js\n│   │   └── b\n│   │       └── b.routes.js\n│   └── c\n│       └── c.routes.json\n└── static\n    ├── css\n    ├── js\n    └── index.html\n```\n\nAnd you loaded the `public` folder into moxy's router like so:\n\n```bash\nnpx @acrontum/moxy --port 5000 --routes ./public\n```\nor\n```typescript\nmoxy.loadConfigFromFile('./public');\n```\n\nthen your route config would look like:\n```json\n{\n  \"/public/routes/a/\": \"...config from a.routes.js file\",\n  \"/public/routes/a/b/\": \"...config from b.routes.js file\",\n  \"/public/routes/c\": \"...config from c.routes.json file\"\n}\n```\n\nIf you instead loaded the `\"a\"` folder (eg. `--routes ./public/a/`, it would look like:\n```json\n{\n  \"/a/\": \"...config from a.routes.js file\",\n  \"/a/b/\": \"...config from b.routes.js file\",\n}\n```\n\n\n## API\n\nSee [full API docs](https://acrontum.github.io/moxy/).\n\n\n### CLI options\n\n```bash\n# npx @acrontum/moxy --help\nStart a mocking server\n\noptions:\n-r, --routes FOLDER       Add routes from FOLDER. Can be called multiple times,\n                          FOLDER can be multiple separated by comma (,).\n-p, --port PORT           Run on port PORT. If none specified, will find an\n                          avaiable port.\n-o, --on CONFIG           Add json CONFIG to routes.\n-q, --quiet               Decrease log verbosity.\n-a, --allow-http-config   Allow routes config via HTTP methods. Default false.\n-v, --version             Show build version and exit.\n-h, --help                Show this menu.\n```\n\n\n### HTTP API\n\nMoxy exposes some HTTP routes for checking routing configurations. With [`allowHttpRouteConfig`](https://acrontum.github.io/moxy/interfaces/ServerConfig.html#router) enabled it will also expose HTTP CRUD routes:\n\n```typescript\nconst server = new MoxyServer({ router: { allowHttpRouteConfig: true } });\n```\n```bash\nnpx @acrontum/moxy --allow-http-config\n```\n\nThe HTTP API offers most of the functionality that programatic or file configs offer, although it is not possible to create and handler as a function at this time.\n\n\n#### GET /\\_moxy:\n\nReturns the list of api methods:\n```js\n{\n  \"GET /router?once=false\u0026serializeMethods=true\": \"show router\",\n  \"GET /routes?once=false\": \"show router routes\",\n  // below are only available with '--allow-http-config'\n  \"POST /routes?once=false\": \"create route\",\n  \"PUT /routes/:route\": \"create or update route\",\n  \"PATCH /routes/:route\": \"update route\",\n  \"DELETE /routes/:route\": \"delete route\"\n}\n\n```\n\n\n#### GET /\\_moxy/routes\n\nDisplay a list of routes which moxy is listening to with default query params (if applicable).\n\nQuery params:\n\n`once=true`: Show routes which will fire once then be removed (eg for testing).\n\n\n#### GET /\\_moxy/router\n\nWill return the current router config, including response handlers.\n\nQuery params:\n\n`once=true`: Show routes which will fire once then be removed (eg for testing).\n\n`serializeMethods=false`: Don't call `.toString()` on methods (removes some noise).\n\n\n#### POST /\\_moxy/router\n\nWill add a json payload to the router.\n\nThe payload should contain `path` and `config`, where `path` is the router path and `config` is [`RouteConfig`](https://acrontum.github.io/moxy/index.html#routeconfig):\n```json\n{\n  \"path\": \"/some/path\",\n  \"config\": {\n    \"get\": {\n      \"status\": 200\n    }\n  }\n}\n```\n\nQuery params:\n\n`once=true`: As soon as this route is hit, remove it.\n\nThe response will be a 201 containing the newly added route.\n\n\n#### PATCH /\\_moxy/router/:route\n\nWill update the route specified by `:route` (`:route` will match everything after `/router/` including slashes).\n\nPayload: [`RouteConfig`](https://acrontum.github.io/moxy/index.md#routeconfig).\n\nThe response will be a 200 containing the newly updated route.\n\n\n#### PUT /\\_moxy/router/:route\n\nWill replace the route specified by `:route` (`:route` will match everything after `/router/` including slashes).\n\nPayload: [`RouteConfig`](https://acrontum.github.io/moxy/index.md#routeconfig).\n\nThe response will be a 201, or 200 if a route was replaced.\n\n\n#### DELETE /\\_moxy/router/:route\n\nWill delete the route specified by `:route` (`:route` will match everything after `/router/` including slashes).\n\nThe response will be a 200 containing `{ message: 'Ok' }`;\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Facrontum%2Fmoxy","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Facrontum%2Fmoxy","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Facrontum%2Fmoxy/lists"}