{"id":20947817,"url":"https://github.com/alphahydrae/api-copilot","last_synced_at":"2025-06-18T20:34:13.199Z","repository":{"id":30365105,"uuid":"33917635","full_name":"AlphaHydrae/api-copilot","owner":"AlphaHydrae","description":"Write testing or data population scenarios for your APIs.","archived":false,"fork":false,"pushed_at":"2015-04-16T14:21:13.000Z","size":448,"stargazers_count":3,"open_issues_count":2,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-03-13T04:42:54.377Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AlphaHydrae.png","metadata":{"files":{"readme":"README.md","changelog":"CHANGELOG.md","contributing":null,"funding":null,"license":"LICENSE.txt","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null}},"created_at":"2015-04-14T07:36:51.000Z","updated_at":"2024-05-19T09:33:38.000Z","dependencies_parsed_at":"2022-08-22T23:11:03.606Z","dependency_job_id":null,"html_url":"https://github.com/AlphaHydrae/api-copilot","commit_stats":null,"previous_names":[],"tags_count":15,"template":false,"template_full_name":null,"purl":"pkg:github/AlphaHydrae/api-copilot","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlphaHydrae%2Fapi-copilot","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlphaHydrae%2Fapi-copilot/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlphaHydrae%2Fapi-copilot/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlphaHydrae%2Fapi-copilot/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AlphaHydrae","download_url":"https://codeload.github.com/AlphaHydrae/api-copilot/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AlphaHydrae%2Fapi-copilot/sbom","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":260629576,"owners_count":23038949,"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","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":[],"created_at":"2024-11-19T00:13:20.713Z","updated_at":"2025-06-18T20:34:08.189Z","avatar_url":"https://github.com/AlphaHydrae.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# API Copilot\n\n\u003e Write testing or data population scenarios for your APIs.\n\n**[Installation](#installation) \u0026mdash; [Documentation](#documentation) \u0026mdash; [Usage](#usage) \u0026mdash; [Contributing](#contributing) \u0026mdash; [License](#license)**\n\n[![NPM version](https://badge.fury.io/js/api-copilot.svg)](http://badge.fury.io/js/api-copilot)\n[![Build Status](https://travis-ci.org/AlphaHydrae/api-copilot.svg?branch=master)](https://travis-ci.org/AlphaHydrae/api-copilot)\n[![Dependency Status](https://gemnasium.com/AlphaHydrae/api-copilot.svg)](https://gemnasium.com/AlphaHydrae/api-copilot)\n[![License](https://img.shields.io/npm/l/api-copilot.svg)](http://opensource.org/licenses/MIT)\n\n**Write an API scenario to make HTTP calls on your API:**\n\n```js\nvar _ = require('underscore'),\n    copilot = require('api-copilot');\n\nvar productsData = [\n  [ 'Cheese', 2.45 ],\n  [ 'Milk', 4 ],\n  [ 'Wine', 10 ]\n];\n\n// create an API scenario\nvar scenario = new copilot.Scenario({\n  name: 'My Scenario',\n  summary: 'Populate some data into my API.',\n  baseUrl: 'http://example.com/api',\n  defaultRequestOptions: {\n    json: true\n  }\n});\n\n// define steps to run\nscenario.step('create a user', function() {\n\n  // make HTTP calls\n  return this.post({\n    url: '/users',\n    body: {\n      name: 'pgibbons',\n      password: 'changeme'\n    }\n  });\n});\n\n// each step gets the result from the previous step\nscenario.step('create 3 products', function(response) {\n\n  var user = response.body;\n\n  var requests = _.map(productsData, function(data) {\n    return this.post({\n      url: '/products',\n      body: {\n        title: data[0],\n        price: data[1],\n        userKey: user.key\n      },\n      expect: { statusCode: 201 }\n    });\n  }, this);\n\n  // run HTTP requests in parallel\n  return this.all(requests);\n});\n\nscenario.step('show created data', function(responses) {\n\n  var products = _.pluck(responses, 'body');\n\n  console.log(products.length + ' products created:');\n  _.each(products, function(product) {\n    console.log('- ' + product.title);\n  });\n});\n\n// export the scenario for API Copilot to use\nmodule.exports = scenario;\n```\n\n**Run it with API Copilot:**\n\n```\napi-copilot --log debug run myScenario\n```\n\n**See it in action:**\n\n```\nMy Scenario\nBase URL: none\n\nSTEP 1: create a user\nCompleted in 140ms\n\nSTEP 2: create 3 products\nCompleted in 1250ms\n\nSTEP 3: show created data\n3 products created:\n- Cheese\n- Milk\n- Wine\nCompleted in 2ms\n\nDONE in 1.39s!\n```\n\n\n\n\n\n## Installation\n\nTo set up your project to use API Copilot for the first time, follow the [project setup procedure](#setup-project).\nIf you simply want to run API Copilot scenarios in a project where the setup procedure has already been done, jump to the [usage installation procedure](#setup-usage).\n\n\u003ca name=\"setup-project\"\u003e\u003c/a\u003e\n### Project Setup Procedure\n\nTo set up a Node.js project to use API Copilot, install `api-copilot` as a development dependency:\n\n    npm install --save-dev api-copilot\n\nYou can run this again whenever you want to upgrade to the latest version of API Copilot.\n\nIf your project is in another language, you can add this minimal `package.json` file to your project:\n\n```json\n{\n  \"name\": \"my-project\",\n  \"version\": \"1.0.0\",\n  \"private\": true,\n  \"devDependencies\": {\n    \"api-copilot\": \"^0.3.3\"\n  }\n}\n```\n\nYou should add the `node_modules` directory to your version control ignore file.\n\nBy default, API Copilot expects your project to have an `api` directory containing **scenarios**.\nEach scenario is a Node.js file that can be run by API Copilot.\nIt must end with `.scenario.js`.\nThe structure of a project containing only API Copilot scenarios might look like this:\n\n```txt\napi/foo.scenario.js\napi/bar.scenario.js\napi/baz.scenario.js\npackage.json\n```\n\n\u003ca name=\"setup-usage\"\u003e\u003c/a\u003e\n### Usage Installation Procedure\n\nInstall the command line interface (you might have to use `sudo`):\n\n    npm install -g api-copilot-cli\n\nIn your project's directory, install API Copilot and other dependencies:\n\n    cd /path/to/project\n    npm install\n\nYou're now ready to [run API Copilot](#cli).\nIf you don't have any API scenarios yet, you might want to [write some](#writing-api-scenarios).\n\n\n\n### Requirements\n\n* [Node.js](http://nodejs.org) v0.10+\n\n\n\n\n\n## Documentation\n\nThis README is the main usage documentation.\n\nRun `api-copilot --help` for command line usage information.\n\nAPI Copilot uses external libraries to provide some of its functionality; refer to their documentation for more information:\n\n* [HTTP requests with the request library](https://github.com/mikeal/request)\n* [Promises with the q library](https://github.com/kriskowal/q)\n\nThe source code is also heavily commented and run through [Docker](https://github.com/jbt/docker), so you can read the resulting [**annotated source code**](http://lotaris.github.io/api-copilot/annotated/index.js.html) for more details about the implementation. It also contains inline examples.\n\nCheck the [CHANGELOG](CHANGELOG.md) for information about new features and breaking changes.\n\n\n\n\n\n## Usage\n\n\u003ca name=\"toc\"\u003e\u003c/a\u003e\n\n* [Writing API Scenarios](#writing-api-scenarios)\n* [Running Scenarios from the Command Line](#cli)\n  * [Listing available scenarios](#listing)\n  * [Getting information about a scenario](#info)\n  * [Running a scenario](#running)\n* [Configuration Options](#configuration-options)\n  * [Changing the configuration while a scenario is running](#changing-the-configuration-while-a-scenario-is-running)\n* [Scenario Flow Control](#scenario-flow-control)\n  * [Completing a step](#step-complete)\n  * [Skipping a step](#step-skip)\n  * [Failing](#step-fail)\n  * [Completing the scenario](#scenario-complete)\n  * [Asynchronous steps](#step-async)\n  * [Change step order](#step-goto)\n* [Making HTTP calls](#making-http-calls)\n  * [Default request options](#default-request-options)\n  * [Request filters](#request-filters)\n  * [Expecting a specific response](#request-expect)\n  * [Running requests in parallel](#request-parallel)\n  * [Configuring the request pipeline](#request-pipeline)\n* [Runtime Parameters](#runtime-parameters)\n  * [Parameter options](#parameter-options)\n  * [Loading parameters from another source](#loading-parameters)\n  * [Documenting parameters](#documenting-parameters)\n* [Multipart Form Data](#multipart-form-data)\n* [Documenting Your Scenarios](#documenting)\n\n\n\n### Writing API Scenarios\n\nThe first to do in a scenario file is require `api-copilot` and create a `Scenario` object:\n\n```js\nvar copilot = require('api-copilot');\n\nvar scenario = new copilot.Scenario({\n  name: 'My Demo Sample Data'\n});\n```\n\nA scenario is basically a sequence of steps that you define using the `step` method.\nThe data returned by each step is available in the next step.\n\n```js\nscenario.step('create some data', function() {\n  return 'some data';\n});\n\nscenario.step('log the data', function(data) {\n  console.log(data);\n});\n```\n\nSteps are executed in the order they are defined by default.\nSee [Flow Control](#scenario-flow-control) for more advanced behavior.\n\nAt the end of the file, you should export the scenario object:\n\n```js\nmodule.exports = scenario;\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n\u003ca name=\"cli\"\u003e\u003c/a\u003e\n### Running Scenarios from the Command Line\n\nThe `api-copilot` command is provided by the separate [api-copilot-cli](https://github.com/lotaris/api-copilot-cli) package.\nInstall it with `npm install -g api-copilot-cli`.\n\nAPI Copilot has two sub-commands which are documented below: `list` and `run`.\nYou may run `api-copilot --help` for command line instructions.\n\n\u003ca name=\"listing\"\u003e\u003c/a\u003e\n#### Listing available scenarios\n\nUse `api-copilot list` to list the scenarios available in the current source directory:\n\n```bash\n$\\\u003e cd /path/to/project \u0026\u0026 api-copilot list\n\nSource directory: /path/to/project/api\n  (use the `-s, --source \u003cdir\u003e` option to list API scenarios from another directory)\n\nAvailable API scenarios (3):\n1) api/foo.scenario.js       (foo)\n2) api/bar.scenario.js       (bar)\n3) api/sub/baz.scenario.js   (baz)\n\nRun `api-copilot info [scenario]` for more information about a scenario.\nRun `api-copilot run [scenario]` to run a scenario.\n[scenario] may be either the number, path or name of the scenario.\n```\n\nBy default, the source directory is the `api` directory relative to the current working directory (where you run API Copilot from).\nFor example, if you are in `/path/to/project`, API Copilot will look for scenarios in `/path/to/project/api`.\nYou may set a different source directory with the `-s, --source \u003cdir\u003e` option.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"info\"\u003e\u003c/a\u003e\n#### Getting information about a scenario\n\nUse `api-copilot info [scenario]` to get detailed information about a scenario.\n\n```bash\n$\\\u003e cd /path/to/project \u0026\u0026 api-copilot info api/my.scenario.js\n\nAPI COPILOT SCENARIO\n\n  Populates some data into my API.\n\n  Name: My Scenario\n  File: /path/to/project/api/my.scenario.js\n\nPARAMETERS (3)\n\n  foo=value (required)\n    This is a required parameter.\n\n  bar=/^https?:/\n    This should be an HTTP or HTTPS URL.\n\n  baz\n    Activate some feature with this flag.\n\nBASE CONFIGURATION\n\n  Options given to the scenario object:\n    {\n      \"name\": \"My Scenario\"\n    }\n\nEFFECTIVE CONFIGURATION\n\n  {\n    \"name\": \"My Scenario\",\n    \"log\": \"info\",\n    \"source\": \"samples\"\n  }\n\nSTEPS (3)\n\n  1. do stuff\n  2. do more stuff\n  3. check stuff\n```\n\nThe `info` command without a scenario argument will behave like the [run command](#running):\nit will automatically run a single available scenario, or ask you which one you want to run if multiple scenarios are available.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"running\"\u003e\u003c/a\u003e\n#### Running a scenario\n\nUse `api-copilot run [scenario]` to run a scenario in the source directory.\n\nIf there is only one scenario available, it will be run automatically.\nIf multiple scenarios are found, API Copilot will display the list and ask you which one you want to run:\n\n```bash\n$\\\u003e cd /path/to/project \u0026\u0026 api-copilot run\n\nSource directory: /path/to/project/api\n  (use the `-s, --source \u003cdir\u003e` option to list API scenarios from another directory)\n\nAvailable API scenarios (3):\n1) api/foo.scenario.js       (foo)\n2) api/bar.scenario.js       (bar)\n3) api/sub/baz.scenario.js   (baz)\n\nType the number, path or name of the scenario you want to run:\n```\n\nAuto-completion is provided for scenario names.\n\nIf multiple scenarios are available, you can also directly run one by specifying its number, path or name as argument to the `run` command:\n\n```bash\napi-copilot run 1\napi-copilot run api/foo.scenario.js\napi-copilot run foo\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n\u003ca name=\"configuration-options\"\u003e\u003c/a\u003e\n\n### Configuration Options\n\nAPI Copilot can be given configuration options in multiple ways:\n\n* as options to the Scenario object;\n* from [YAML](http://www.yaml.org) configuration files:\n  * the `.api-copilot.yml` file in your home directory;\n  * the `api-copilot.yml` file in the current working directory;\n  * other configuration files specified with the `config` option;\n* from environment variables;\n* as options on the command line (run `api-copilot --help` to see available options).\n\nEach of these option sources has greater precedence than the previous one.\nFor example, an option given on the command line will always overwrite the value of the same option read from a configuration file or given through an environment variable.\n\nThe following configuration options are supported:\n\n* `log` \u0026mdash; command line `-l, --log \u003clevel\u003e` \u0026mdash; env var `API_COPILOT_LOG=\u003clevel\u003e`\n\n  Log level (trace, debug or info). The default level is info. Use trace to see the stack trace of errors.\n\n* `source` \u0026mdash; command line `-s, --source \u003cdir\u003e` \u0026mdash; env var `API_COPILOT_SOURCE=\u003cdir\u003e`\n\n  Path to the directory where API scenarios are located.\n  The default directory is `api`.\n  The path can be absolute or relative to the current working directory.\n\n* `baseUrl` \u0026mdash; command line `-u, --base-url \u003curl\u003e` \u0026mdash; env var `API_COPILOT_BASE_URL=\u003curl\u003e`\n\n  Override the base URL of the scenario.\n  Only the paths of URLs will be printed in debug mode.\n\n* `showTime` \u0026mdash; command line `-t, --show-time` \u0026mdash; env var `API_COPILOT_SHOW_TIME=1`\n\n  Print the date and time with each log.\n\n* `showRequest` \u0026mdash; command line `-q, --show-request` \u0026mdash; env var `API_COPILOT_SHOW_REQUEST=1`\n\n  Print options for each HTTP request (only with debug or trace log levels).\n\n* `showResponseBody` \u0026mdash; command line `-b, --show-response-body` \u0026mdash; env var `API_COPILOT_SHOW_RESPONSE_BODY=1`\n\n  Print response body for each HTTP request (only with debug or trace log levels).\n\n* `showFullUrl` \u0026mdash; command line `--show-full-url` \u0026mdash; env var `API_COPILOT_SHOW_FULL_URL=1`\n\n  Always print full URLs even when a base URL is configured (only with debug or trace log levels).\n\n* `requestPipeline` \u0026mdash; command line `--request-pipeline \u003cn\u003e` \u0026mdash; env var `API_COPILOT_REQUEST_PIPELINE=\u003cn\u003e`\n\n  Maximum number of HTTP requests to run in parallel (no limit by default).\n  See [request pipeline](#request-pipeline).\n\n* `requestCooldown` \u0026mdash; command line `--request-cooldown \u003cms\u003e` \u0026mdash; env var `API_COPILOT_REQUEST_COOLDOWN=\u003cms\u003e`\n\n  If set and an HTTP request ends, no other request will be started before this time (in milliseconds) has elapsed (no cooldown by default).\n  See [request pipeline](#request-pipeline).\n\n* `requestDelay` \u0026mdash; command line `--request-delay \u003cms\u003e` \u0026mdash; env var `API_COPILOT_REQUEST_DELAY=\u003cms\u003e`\n\n  If set and an HTTP request starts, no other request will be started before this time (in milliseconds) has elapsed (no delay by default).\n  See [request pipeline](#request-pipeline).\n\n  \u003ca name=\"summary-option\"\u003e\u003c/a\u003e\n\n* `summary`\n\n  Short summary explaining what your scenario does.\n  This will be displayed in the [info command](#info) output.\n\n  This option cannot be changed on the command line.\n\n\u003ca name=\"configuration-files\"\u003e\u003c/a\u003e\n\nAdditionally, these command-line- and environment-only options can be used to customize from which configuration files options are read from:\n\n* `-c, --config \u003cfile\u003e` \u0026mdash; env var `API_COPILOT_CONFIG=\u003cfile\u003e`\n\n  Add a configuration file to read options from.\n  The path can be absolute or relative to the current working directory.\n  This option can be used multiple times.\n\n  **WARNING:** note that by default, API Copilot will attempt to read `~/.api-copilot.yml` and `$PWD/api-copilot.yml`.\n  Using the `--config` option disables these default configuration files unless you also set the `--default-configs` option.\n\n* `--default-configs` \u0026mdash; env var `API_COPILOT_DEFAULT_CONFIGS=1`\n\n  In conjunction with the `--config` option, this causes the default configuration files\n  (`~/.api-copilot.yml` and `$PWD/api-copilot.yml`) to still be read instead of being ignored.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n#### Changing the Configuration while a Scenario is Running\n\nIn any step of the scenario, you may change the configuration with the `configure` method:\n\n```js\nvar scenario = new Scenario({\n  name: 'myScenario',\n  summary: 'Populates some data into my API.',\n  baseUrl: 'http://example.com/foo'\n});\n\nscenario.step('first step', function() {\n\n  // this HTTP request will use the baseUrl configured above\n  return this.get({ url: '/' });\n});\n\nscenario.step('second step', function(response) {\n  \n  // change the baseUrl\n  this.configure({ baseUrl: 'http://example.com/bar' });\n\n  // this HTTP request will use the newly configured baseUrl\n  return this.get({ url: '/' });\n});\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n### Scenario Flow Control\n\n\u003ca name=\"step-complete\"\u003e\u003c/a\u003e\nTo **complete a step** and send a result to the next step, you can simply return the result:\n\n```js\nscenario.step('compute data', function() {\n  return computeSomeDataSynchronously();\n});\n\nscenario.step('log data', function(data) {\n  console.log(data);\n});\n```\n\nNote that this only works for *synchronous* steps.\n[Asynchronous steps](#step-async) and [HTTP Calls](#making-http-calls) are described later.\n\nIf you want to **pass multiple results** to the next step, use the `success` method:\n\n```js\nscenario.step('compute data', function() {\n  return this.success('foo', 'bar', 'baz');\n});\n\nscenario.step('log data', function(result1, result2, result3) {\n  console.log(result1);\n  console.log(result2);\n  console.log(result3);\n});\n```\n\nAgain, this is only for synchronous steps.\n\n\u003ca name=\"step-skip\"\u003e\u003c/a\u003e\nTo **skip a step** and log an informational message, use the `skip` method:\n\n```js\nscenario.step('compute data', function() {\n  if (someCondition) {\n    return this.skip('no computation needed');\n  } else {\n    return 'some data';\n  }\n});\n```\n\n\u003ca name=\"step-fail\"\u003e\u003c/a\u003e\nTo **fail a step** and stop the whole scenario, use the `fail` method:\n\n```js\nscenario.step('log data', function(data) {\n  if (data == null) {\n    return this.fail('data was not computed');\n  }\n  console.log(data);\n});\n```\n\nOr you can simply throw an error:\n\n```js\nscenario.step('log data', function(data) {\n  if (data == null) {\n    throw new Error('data was not computed');\n  }\n  console.log(data);\n});\n```\n\n\u003ca name=\"scenario-complete\"\u003e\u003c/a\u003e\nTo **complete the scenario** successfully and not run any more steps, use the `complete` method:\n\n```js\nscenario.step('might be done here', function(done) {\n  if (done) {\n    // further steps will not be executed\n    return this.complete();\n  }\n\n  // otherwise continue\n  return computeSomeStuff();\n});\n```\n\n\u003ca name=\"step-async\"\u003e\u003c/a\u003e\nTo make an **asynchronous step**,\nreturn a [promise](http://promises-aplus.github.io/promises-spec/) instead of a value (see the [q](https://github.com/kriskowal/q) library).\n\nAPI Copilot provides you the `defer` method to generate a deferred object.\nThis object has a promise property which you can return;\nthen resolve or reject the deferred object once your asynchronous operation is complete.\n\n```js\nscenario.step('async step', function() {\n\n  var deferred = this.defer();\n\n  // this operation is asynchronous, so it will be executed later\n  asyncStuff('input', function(err, result) {\n    if (err) {\n      deferred.reject(err);\n    } else {\n      deferred.resolve(result);\n    }\n  });\n\n  // return the promise object\n  return deferred.promise;\n});\n```\n\nYou can simplify callback-based asynchronous code by requiring [q](https://github.com/kriskowal/q) and using its [Node adaptation functions](https://github.com/kriskowal/q#adapting-node):\n\n```js\nvar q = require('q');\n\nscenario.step('async step', function() {\n  return q.nfcall(asyncStuff, 'input');\n});\n```\n\nTo do this, you must add `q` to your own development dependencies by running `npm install --save-dev q`.\n\nYou may also return any promise that follows the [Promises/A+ standard](http://promises-aplus.github.io/promises-spec/).\n\n\u003ca name=\"step-goto\"\u003e\u003c/a\u003e\nTo **continue with another step than the next one**, call the `setNextStep` method at any time during the execution of the current step:\n\n```js\nscenario.step('first step', function() {\n  this.setNextStep('third step');\n  return this.success('some data');\n});\n\nscenario.step('second step', function() {\n  // this step will be skipped\n});\n\nscenario.step('third step', function(data) {\n  console.log(data);\n});\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n### Making HTTP Calls\n\nAPI Copilot includes the [request](https://github.com/mikeal/request) library to make HTTP calls.\n\nUse the `get`, `head`, `post`, `put`, `patch` and `delete` methods to start a call.\nThese methods return a promise thath you can return from your step.\nThe next step will receive the HTTP response.\n\n```js\nscenario.step('call API', function() {\n  return this.get({\n    url: 'http://example.com/'\n  });\n});\n\nscenario.step('log response body', function(response) {\n  console.log(response.body); // response body string\n});\n```\n\nYou can also use the more generic `request` method that supports any HTTP verb:\n\n```js\nscenario.step('an API call', function() {\n  return this.request({\n    method: 'GET',\n    url: 'http://example.com/'\n  });\n});\n```\n\nIf an I/O error occurs, the error message will be logged and the scenario will stop.\n\nNote that server errors do not interrupt the scenario.\nIt is your responsibility to check the response from the server:\n\n```js\nscenario.step('an API call', function() {\n  return this.get({\n    url: 'http://example.com'\n  });\n});\n\nscenario.step('handle API data', function(response) {\n  if (response.statusCode != 200) {\n    return this.fail('server responded with unexpected status code ' + response.statusCode);\n  }\n  console.log(response.body);\n});\n```\n\nPassing the **json option** causes the request body and response body to be serialized/deserialized as JSON:\n\n```js\nscenario.step('an API call', function() {\n  return this.post({\n    url: 'http://example.com',\n    json: true,\n    body: {\n      foo: 'bar'\n    }\n  }); // the request body will be {\"foo\":\"bar\"}\n});\n\nscenario.step('handle API data', function(response) {\n  console.log(response.body); // the body will be a javascript object: { foo: 'bar' }\n});\n```\n\nRead the [request documentation](https://github.com/mikeal/request#requestoptions-callback) for more HTTP configuration options.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n#### Default Request Options\n\nIf you need to re-use options for many requests, you can use `setDefaultRequestOptions` in a step.\nIt will apply the specified options to all subsequent requests.\n\n```js\nscenario.step('step 1', function() {\n\n  this.setDefaultRequestOptions({\n    json: true\n  });\n\n  // the `json` option will be added to this request\n  return this.post({\n    url: '/foo',\n    body: {\n      some: 'data'\n    }\n  });\n});\n\nscenario.step('step 2', function(response) {\n\n  // this request will also have the `json` option\n  return this.post({\n    url: '/bar',\n    body: {\n      more: 'data'\n    }\n  });\n});\n```\n\nYou can also extend default request options (without overriding all of them) with `extendDefaultRequestOptions`:\n\n```js\nscenario.step('step 3', function(response) {\n\n  this.extendDefaultRequestOptions({\n    headers: {\n      'X-Custom': 'value',\n      'X-Custom-2': 'value 2'\n    }\n  });\n\n  // this request will have both the `json` and `headers` options\n  return this.post({\n    url: '/baz',\n    body: {\n      more: 'data'\n    }\n  });\n});\n```\n\n\u003ca name=\"defaultRequestOptions-merge\"\u003e\u003c/a\u003e\nIf you need to add options to a sub-object, like `headers`, use `mergeDefaultRequestOptions`:\n\n```js\nscenario.step('step 4', function(response) {\n\n  this.mergeDefaultRequestOptions({\n    headers: {\n      // add a header without overriding previous ones\n      'X-Custom-B': 'value B',\n      // also clear a specific header without clearing them all\n      'X-Custom-2': undefined\n    }\n  });\n\n  // this request will have the X-Custom and X-Custom-B headers,\n  // while the X-Custom-2 header was cleared\n  return this.post({\n    url: '/baz',\n    body: {\n      more: 'data'\n    }\n  });\n});\n```\n\nClear previously configured default request options with `clearDefaultRequestOptions`:\n\n```js\nscenario.step('step 5', function(response) {\n\n  // clear specific request option(s) by name\n  this.clearDefaultRequestOptions('headers');\n\n  // this request will only have the `json` option added since the `headers` option was cleared\n  return this.post({\n    url: '/qux',\n    body: {\n      more: 'data'\n    }\n  });\n});\n\nscenario.step('step 6', function(response) {\n\n  // clear all default request options\n  this.clearDefaultRequestOptions();\n\n  // no additional options will be added\n  return this.post({\n    url: '/quxx',\n    body: 'some text'\n  });\n});\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n#### Request Filters\n\nRequest filters are run just before an HTTP call is made, allowing you to customize the request based on all its options:\n\n```js\n// define a filter function\n// the `requestOptions` argument will be the actual options passed to the request library\nfunction signRequest(requestOptions) {\n\n  requestOptions.headers = {\n    'X-Signature': sha1(requestOptions.method + '\\n' + requestOptions.url)\n  };\n\n  // the filter function must return the updated request options\n  return requestOptions;\n}\n\nscenario.step('HTTP call with signature authentication', function() {\n\n  // add the filter\n  this.addRequestFilter(signRequest);\n\n  // the X-Signature header will automatically be added to this and subsequent requests\n  this.get({\n    url: '/foo'\n  });\n});\n```\n\nRemove request filters with `removeRequestFilters`:\n\n```js\nscenario.step('another step', function() {\n\n  // remove specific request filter(s)\n  this.removeRequestFilters(signRequest);\n\n  // remove all requests filters\n  this.removeRequestFilters();\n});\n```\n\nYou can also identify filters by name:\n\n```js\nscenario.step('HTTP call with named filters', function() {\n\n  // add named filters\n  this.addRequestFilter('foo', fooFilter);\n  this.addRequestFilter('bar', barFilter);\n  this.addRequestFilter('baz', bazFilter);\n\n  // adding a new filter for the same name overrides the previous filter\n  this.addRequestFilter('foo', anotherFooFilter);\n\n  // remove filters with a given name or names\n  this.removeRequestFilters('foo');\n  this.removeRequestFilters('bar', 'baz');\n\n  // remove all request filters\n  this.removeRequestFilters();\n});\n```\n\nYou can make a request filters asynchronous by returning a promise for the request options:\n\n```js\nscenario.step('asynchronous filters', function() {\n\n  this.addRequestFilter('signature', function(requestOptions) {\n\n    // make a deferred object with the q library\n    var deferred = q.defer();\n\n    // launch your asynchronous operation\n    getSomeAsyncRequestOptions(requestOptions, function(err, options) {\n      if (err) {\n        return deferred.reject(err);\n      }\n\n      // resolve the deferred object when done\n      deferred.resolve(options);\n    });\n\n    // return the promise\n    return deferred.promise;\n  });\n\n  this.get({\n    url: '/foo'\n  });\n});\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"request-expect\"\u003e\u003c/a\u003e\n#### Expecting a Specific Response\n\nYou can specify expected properties of an HTTP response with the `expect` option.\n\nTo check that the status code is the one you expect, specify an expected `statusCode`:\n\n```js\nscenario.step('step', function() {\n\n  return this.post({\n    url: 'http://example.com/foo',\n    body: {\n      some: 'data'\n    },\n    expect: {\n      // the request will fail and the scenario will be interrupted\n      // if the status code of the response is not the expected one\n      statusCode: 201\n    }\n  });\n});\n```\n\nCheck that the code is in a given range using a regular expression:\n\n```js\nscenario.step('step', function() {\n\n  return this.post({\n    url: 'http://example.com/foo',\n    body: {\n      some: 'data'\n    },\n    expect: {\n      // the request will fail and the scenario will be interrupted\n      // if the status code of the response is not in the 2xx range\n      statusCode: /^2/\n    }\n  });\n});\n```\n\nYou can also specify an array of expected status codes:\n\n```js\nscenario.step('step', function() {\n  return this.get({\n    url: 'http://example.com',\n    expect: {\n      // the request will fail and the scenario will be interrupted\n      // if the status code of the response is not among these\n      statusCode: [ 200, 204, /^3/ ]\n    }\n  });\n});\n```\n\n\u003ca name=\"request-expect-custom-message\"\u003e\u003c/a\u003e\n\nTo specify a custom error message, pass an object with the expected\nvalue as the `value` option, and the message as the `message` option:\n\n```js\nscenario.step('step', function() {\n  return this.get({\n    url: 'http://example.com',\n    expect: {\n      statusCode: {\n        value: /^2/,\n        message: 'Must be in the 2xx range.'\n      }\n    }\n  });\n});\n```\n\nTo build an error message from the expected and actual values,\npass a function as the `message` option:\n\n```js\nscenario.step('step', function() {\n  return this.get({\n    url: 'http://example.com',\n    expect: {\n      statusCode: {\n        value: [ 200, 201, 204 ],\n        message: function(expected, actual) {\n          return \"Expected \" + actual + \" to be in \" + expected;\n        }\n      }\n    }\n  });\n});\n```\n\n\u003ca name=\"request-parallel\"\u003e\u003c/a\u003e\n#### Running requests in parallel\n\nPass an array of HTTP requests to the `all` method to run them in parallel:\n\n```js\nscenario.step('many HTTP calls', function() {\n  return this.all([\n    this.get('http://example.com/foo'),\n    this.get('http://example.com/bar'),\n    this.get('http://example.com/baz')\n  ]);\n});\n\nscenario.step('handle responses', function(responses) {\n  // you get the three responses in the next step\n  // when all requests have completed\n  responses.length; // #=\u003e 3\n});\n```\n\nThe `all` method turns an array of promises into a single promise that is resolved\nwhen all the promises in the array have been resolved. The resolution value will be\nan array of the resolved values.\n\nRead about [q combinations](https://github.com/kriskowal/q#combination) for more information.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"request-pipeline\"\u003e\u003c/a\u003e\n#### Configuring the request pipeline\n\n[Parallelism](#request-parallel) might be an issue in some situations.\nMaybe your backend cannot handle as many concurrent requests as are issued by your scenario,\nor maybe your API limits the frequency of calls.\n\nThe request pipeline allows you to limit how often HTTP requests are started.\nIt has three [configuration options](#configuration-options).\n\n* **requestPipeline** \u0026mdash; `\u003cn\u003e`\n\n  This limits the number of HTTP requests that can be made in parallel.\n  For example, if set to 3, no more than 3 HTTP requests will run concurrently at any given time.\n\n```js\nscenario.step('limit request concurrency', function() {\n\n  scenario.configure({ requestPipeline: 3 });\n\n  return this.all([\n\n    // the first three requests will be executed concurrently\n    this.get({ url: 'http://example.com/a' }),   // starts at time 0, takes 50ms\n    this.get({ url: 'http://example.com/b' }),   // starts at time 0, takes 100ms\n    this.get({ url: 'http://example.com/c' }),   // starts at time 0, takes 100ms\n\n    // the next three requests will start one by one as soon as each of the first three finishes\n    this.get({ url: 'http://example.com/d' }),   // starts at time 50, after the first request finishes\n    this.get({ url: 'http://example.com/e' }),   // starts at time 100, after the second request finishes\n    this.get({ url: 'http://example.com/f' })    // starts at time 100, after the third request finishes\n  ]);\n});\n```\n\n* **requestCooldown** \u0026mdash; `\u003cms\u003e`\n\n  When set, the request cooldown guarantees that after an HTTP request end,\n  no other request will start before the cooldown time (in milliseconds) has elapsed.\n\n```js\nscenario.step('wait after each request', function() {\n\n  scenario.configure({ requestPipeline: 1, requestCooldown: 250 });\n\n  // after the first HTTP request, each request will wait for the previous one to finish,\n  // since the request pipeline is set to 1, and then 250 additional milliseconds (the\n  // request cooldown), before starting\n  return this.all([\n    this.get({ url: 'http://example.com/a' }),   // starts at time 0, takes 100ms\n    this.get({ url: 'http://example.com/b' }),   // starts at time 350 (last response time + cooldown), takes 100ms\n    this.get({ url: 'http://example.com/c' }),   // starts at time 700 (last response time + cooldown), takes 100ms\n    this.get({ url: 'http://example.com/d' }),   // starts at time 1050 (last response time + cooldown), takes 100ms\n  ]);\n});\n```\n\n* **requestDelay** \u0026mdash; `\u003cms\u003e`\n\n  When set, the request delay guarantees that after an HTTP request starts,\n  no other request will start before the delay time (in milliseconds) has elapsed.\n  This can be used to spread out concurrent requests so that they are not all\n  started at exactly the same time.\n\n```js\nscenario.step('wait after each request', function() {\n\n  scenario.configure({ requestDelay: 50 });\n\n  // each request will start 50ms after the previous one has started\n  // but they will still run concurrently if they take more than 50ms to complete\n  return this.all([\n    this.get({ url: 'http://example.com/a' }),   // starts at time 0\n    this.get({ url: 'http://example.com/b' }),   // starts at time 50\n    this.get({ url: 'http://example.com/c' }),   // starts at time 100\n    this.get({ url: 'http://example.com/d' }),   // starts at time 150\n  ]);\n});\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n### Runtime Parameters\n\nScenarios can be configured to take custom runtime parameters with the `addParam` method:\n\n```js\nvar scenario = new Scenario({\n  name: 'scenario with parameters'\n});\n\nscenario.addParam('foo');\nscenario.addParam('bar', { flag: true });\n```\n\nThese parameters can then be supplied on the command line:\n\n```\n#\u003e api-copilot -p foo=value -p bar\n```\n\nRetrieve them with the `param` method when running the scenario:\n\n```js\nscenario.step('step', function() {\n  this.param('foo');   // \"value\"\n  this.param('bar');   // true\n});\n```\n\nNote that all parameters must be registered with the `addParam` method.\nTrying to retrieve unknown parameters will throw an error and stop the scenario.\n\n```js\nscenario.step('step', function() {\n  this.param('unknown'); // throws Error\n});\n```\n\nUse the [info command](#info) to obtain a list of the parameters for a scenario.\nSee [parameter options](#parameter-options) on how to configure and document parameters.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"parameter-options\"\u003e\u003c/a\u003e\n#### Parameter options\n\n\u003ca name=\"parameter-option-required\"\u003e\u003c/a\u003e\n\n* **required** \u0026mdash; `boolean, default: true`\n\n  Parameters are required by default.\n  Set this option to `false` to make them optional.\n\n```js\nscenario.addParam('optionalFeature', {\n  required: false\n});\n```\n\n  If you try to run a scenario without giving a value for a required parameter, you will be prompted to enter a value:\n\n```\nThe scenario cannot be run because the following parameters are either missing or invalid:\n- foo is required; set it with `-p foo=value` or `--param foo=value`\n\nYou will now be asked for the missing or corrected values.\nPress Ctrl-C to quit.\n\nfoo=value (required)\n  This is a required parameter.\n\nEnter a value for foo: \n```\n\n\u003ca name=\"parameter-option-default\"\u003e\u003c/a\u003e\n\n* **default** \u0026mdash; `any, default: undefined`\n\n  Default value of the parameter when not configured.\n\n```js\nscenario.addParam('url',  {\n  default: 'http://example.com'\n});\n```\n\n\u003ca name=\"parameter-option-flag\"\u003e\u003c/a\u003e\n\n* **flag** \u0026mdash; `boolean, default: false`\n\n  Set this option to `true` to make your parameter a boolean flag.\n\n  Boolean flags are specified without a value on the command line: `-p foo` or `--params foo`.\n  Trying to give them a value will produce an error and prevent the scenario from running.\n\n  When retrieving the value with the `param` method in a scenario, it will be either `true` or `undefined` (if the flag was not given).\n\n```js\nscenario.addParam('coolFeature', {\n  flag: true\n});\n```\n\n* **pattern** \u0026mdash; `regexp, default: none`\n\n  Validate your parameter with a regular expression.\n\n  The scenario will only run if the supplied parameter value matches;\n  otherwise, you will be prompted for a new value.\n\n```js\nscenario.addParam('backendUrl', {\n  pattern: /^https?:/\n});\n```\n\n\u003ca name=\"parameter-option-description\"\u003e\u003c/a\u003e\n\n* **description** \u0026mdash; `string, default: none`\n\n  Additional documentation for your parameter.\n  It will be displayed in the [info command](#info) output.\n\n```js\nscenario.addParam('backendUrl', {\n  description: 'The URL to our cool backend. Must be HTTPS.'\n});\n```\n\nSample output:\n\n```\nbackendUrl=value (required)\n  The URL to our cool backend. Must be HTTPS.\n```\n\n* **valueDescription** \u0026mdash; `string, default: none`\n\n  Custom value description for your parameter.\n\n  The default description of a parameter is `foo=value`.\n  If you specified a `pattern` option, it will print the pattern instead, e.g. `backendUrl=/^https?:/`.\n\n  Set a `valueDescription` to customize this.\n\n```js\nscenario.addParam('backendUrl', {\n  valueDescription='url'\n});\n```\n\nSample output:\n\n```\nbackendUrl=url\n```\n\n\u003ca name=\"parameter-option-obfuscate\"\u003e\u003c/a\u003e\n\n* **obfuscate** \u0026mdash; `boolean, default: false`\n\n  If set, the value of this parameter will be obfuscated when displayed\n  before the scenario starts running.\n\n  Note that this is automatically set for parameters whose name contains\n  the words `password`, `secret` or `authToken` (case-insensitive). If you\n  have such a parameter that you do not want obfuscated, set the option\n  to `false`.\n\n```js\nscenario.addParam('password');\n\nscenario.addParam('foo', {\n  obfuscate: true\n});\n```\n\nSample output when running a scenario:\n\n```\nRuntime parameters:\n  password = \"************\"\n  foo = \"***\"\n```\n\n\u003ca name=\"parameter-option-processor\"\u003e\u003c/a\u003e\n\n* **processor** \u0026mdash; `function(value, previousValue), default: none`\n\n  If set, the parameter will take the value obtained by calling this function with the configured value.\n  If a default value is set, it will be given as second argument.\n  This can be used to coerce values of a given type, such as numbers.\n\n```js\nscenario.addParam('n', {\n  processor: parseInt\n});\n```\n\n  If a parameter is given multiple times at the command line,\n  or is an array in the scenario object or configuration file,\n  the processor function will be called once for each value, with that value as the first argument.\n  The second argument will be the result returned by the processor function for the previous value, or the default value the first time.\n\n  This can be used to construct a parameter value from multiple values.\n\n```js\n// this scenario can use multiple URLs\nscenario.addParam('urls', {\n\n  // start with an empty list\n  default: [],\n\n  // every time a -p urls=\u003curl\u003e is given, add it to the list\n  processor: function(value, urls) {\n    urls.push(value);\n    return urls;\n  }\n});\n\n// if called with `-p urls=http://example.com/a -p urls=http://example.com/b`,\n// the value of the `urls` parameter will be [ \"http://example.com/a\", \"http://example.com/b\" ]\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"loading-parameters\"\u003e\u003c/a\u003e\n#### Loading parameters from another source\n\nBy default, runtime parameters can come from three sources:\n\n* the options given to the scenario object;\n* the YAML configuration file;\n* command line parameters.\n\nTo load more parameters from elsewhere, add a **loading function** to your scenario object with the `loadParametersWith` method:\n\n```js\nvar scenario = new Scenario({\n  name: 'scenario with lots of parameters',\n  params: { some: 'initial', parameter: 'values' }\n});\n\n// load more parameters from anywhere\nscenario.loadParametersWith(function(params) {\n\n  // each loading function is passed all previously loaded parameters,\n  // including those from options, the configuration file and the command line\n  params.more = 'parameters';\n\n  // each loading function must return the updated parameters\n  return params;\n});\n\n// return a promise to support asynchronous parameter loading\nscenario.loadParametersWith(function() {\n\n  var deferred = q.defer(); // promises with the q library\n\n  loadParametersAsynchronously(function(err, moreParams) {\n    if (err) {\n      return deferred.reject(err);\n    }\n\n    // resolve the promise once the parameters have been loaded\n    deferred.resolve(moreParams);\n  });\n\n  // return a promise for the updated parameters\n  return deferred.promise;\n});\n```\n\nParameter loading functions are run in the order they are added to the scenario object.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\u003ca name=\"documenting-parameters\"\u003e\u003c/a\u003e\n#### Documenting parameters\n\nIn addition to the [description option](#parameter-option-description) shown above,\nthe `addParam` method returns a parameter object which will emit a `describe` event when its documentation is printed.\n\nYou can listen to this event to print more documentation:\n\n```js\nvar myParam = scenario.addParam('myParam');\n\nmyParam.on('describe', function(print) {\n\n  // use the provided print function to have your text correctly indented\n  print('More');\n  print('information.');\n\n  // it supports new lines as well\n  print('Even\\nmore\\ninformation.');\n});\n```\n\nSample info output:\n\n```\nmyParam=value\n  More\n  information.\n  Even\n  more\n  information.\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n### Multipart Form Data\n\nThe [request](https://github.com/mikeal/request) library included in API Copilot supports `multipart/form-data` requests with the [form-data](https://github.com/felixge/node-form-data) library.\nCheck out [its documentation about forms](https://github.com/mikeal/request#forms).\nUsing only the request library, this is how you would upload a file:\n\n```js\nvar fs = require('fs');\n\nvar r = request.post('http://example.com/upload', function(err, response, body) {\n  if (err) {\n    return console.error('Upload failed: ' + err);\n  }\n  console.log('Upload successful! Server responded with: ' + body);\n})\n\nvar form = r.form()\nform.append('my_file', fs.createReadStream('/path/to/file.ext'));\n```\n\nTo retrieve the request object and create a form with API Copilot, you will need to supply a handler function.\nThe handler function is called with the request object before the HTTP request starts.\n\n```js\nvar fs = require('fs');\n\nfunction createFileUploadHandler(file) {\n  return function (request) {\n    var form = request.form();\n    form.append('file', fs.createReadStream(file));\n  };\n}\n\nscenario.step('file upload', function() {\n\n  var file = '/path/to/file.ext';\n\n  return this.post({\n    url: 'http://example.com/',\n    handler: createFileUploadHandler(file)\n  });\n});\n```\n\n**Note:** the request handler must be synchronous since the HTTP request will start immediately at the next tick.\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n\u003ca name=\"documenting\"\u003e\u003c/a\u003e\n### Documenting Your Scenarios\n\nScenarios with many parameters can become quite complicated to understand and use.\n\nStart by specifying a short [summary](#summary-option).\nThen document your parameters with the [description option](#parameter-option-description) and the [describe event](#documenting-parameters).\n\nTo add additional documentation at the end of the [info command](#info) output, listen to the `scenario:info` event on the scenario object:\n\n```js\nvar copilot = require('api-copilot');\n\nvar scenario = new copilot.Scenario({\n  name: 'My Complex Scenario'\n});\n\nscenario.on('scenario:info', function() {\n  console.log('Additional Information:');\n  console.log();\n  console.log('  To use this scenario, you must ... and ... first.');\n  console.log();\n});\n\n```\n\n\u003ca href=\"#toc\" style=\"float:right;\"\u003eBack to top\u003c/a\u003e\n\n\n\n\n\n## Contributing\n\n* [Fork](https://help.github.com/articles/fork-a-repo)\n* Create a topic branch - `git checkout -b feature`\n* Push to your branch - `git push origin feature`\n* Create a [pull request](http://help.github.com/pull-requests/) from your branch\n\nPlease add a changelog entry with your name for new features and bug fixes.\n\n\n\n\n\n## License\n\nAPI Copilot is licensed under the [MIT License](http://opensource.org/licenses/MIT).\nSee [LICENSE.txt](LICENSE.txt) for the full text.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falphahydrae%2Fapi-copilot","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Falphahydrae%2Fapi-copilot","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Falphahydrae%2Fapi-copilot/lists"}