{"id":20605238,"url":"https://github.com/phux/apijc","last_synced_at":"2026-04-21T06:32:18.654Z","repository":{"id":210135181,"uuid":"725829244","full_name":"phux/apijc","owner":"phux","description":"A tool to automate regression testing by comparing responses of URLs on two different domains.","archived":false,"fork":false,"pushed_at":"2023-12-07T12:03:58.000Z","size":48,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"main","last_synced_at":"2025-03-06T16:55:00.586Z","etag":null,"topics":["api","automated-testing","json","regression-testing"],"latest_commit_sha":null,"homepage":"","language":"Go","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/phux.png","metadata":{"files":{"readme":"README.md","changelog":null,"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":"2023-12-01T00:35:13.000Z","updated_at":"2023-12-07T12:10:41.000Z","dependencies_parsed_at":"2024-11-16T09:27:32.346Z","dependency_job_id":"bf94c07e-dc5f-420b-8828-41e7640a7d75","html_url":"https://github.com/phux/apijc","commit_stats":null,"previous_names":["phux/apijc"],"tags_count":5,"template":false,"template_full_name":null,"purl":"pkg:github/phux/apijc","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phux%2Fapijc","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phux%2Fapijc/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phux%2Fapijc/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phux%2Fapijc/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/phux","download_url":"https://codeload.github.com/phux/apijc/tar.gz/refs/heads/main","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/phux%2Fapijc/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":32080284,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-21T06:27:27.065Z","status":"ssl_error","status_checked_at":"2026-04-21T06:27:21.250Z","response_time":128,"last_error":"SSL_connect returned=1 errno=0 peeraddr=140.82.121.5:443 state=error: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"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":["api","automated-testing","json","regression-testing"],"created_at":"2024-11-16T09:27:14.795Z","updated_at":"2026-04-21T06:32:18.638Z","avatar_url":"https://github.com/phux.png","language":"Go","funding_links":[],"categories":[],"sub_categories":[],"readme":"# API JSON Compare (apijc)\n\n`apijc` fetches and compares the status codes and json responses of\nuser-defined paths on two domains and reports any differences.\n\n\u003c!--toc:start--\u003e\n\n- [API JSON Compare (apijc)](#api-json-compare-apijc)\n  - [Features](#features)\n  - [Installation](#installation)\n    - [Binary](#binary)\n    - [Golang](#golang)\n  - [Usage](#usage)\n    - [Quickstart](#quickstart)\n  - [Example output](#example-output)\n  - [Configuration](#configuration)\n    - [CLI Flags](#cli-flags)\n    - [urlFile](#urlfile)\n      - [targets](#targets)\n      - [sequentialTargets](#sequentialtargets)\n      - [Structure](#structure)\n      - [Path expansion](#path-expansion)\n      - [requestBody vs requestBodyFile](#requestbody-vs-requestbodyfile)\n      - [urlFile Example](#urlfile-example)\n    - [rateLimit](#ratelimit)\n    - [headerFile](#headerfile)\n      - [headerFile Example](#headerfile-example)\n      - [Precedence](#precedence)\n    - [Output](#output)\n      - [stdout](#stdout)\n      - [outputFile](#outputfile)\n        - [outputFile Example](#outputfile-example)\n  - [Exit codes](#exit-codes)\n  - [TODOs](#todos)\n  \u003c!--toc:end--\u003e\n\n## Features\n\n- Diff comparison of response bodies from both domains\n- Sequential request chains. See [sequentialTargets](#sequentialtargets) below\n- Check for expected status codes\n- Path expansion of\n  - Lists (example: `/foo/{1,2,3}/bar`)\n  - Numerical ranges (example: `/foo/{1-100}/bar`)\n  - Mixed list and ranges (example: `/foo/{1,3-5,99,200-400}`)\n  - See [Path expansion](#path-expansion) below\n- Rate limiting\n- Load headers from file\n  - Specify header key-value pairs globally or per domain\n- Custom headers per url target\n- Write errors/mismatches to stdout or file\n\n## Installation\n\n### Binary\n\n1. Download the binary for your architecture from the\n   [Releases](https://github.com/phux/apijc/releases) page.\n2. Put it into a directory in your `$PATH`\n\n### Golang\n\n```sh\ngo install github.com/phux/apijc@latest\n```\n\n## Usage\n\n```sh\napijc \\\n  --baseDomain \"\u003chttp://first.domain\u003e\"  \\\n  --newDomain \"\u003chttp://second.domain\u003e\" \\\n  --urlFile \u003cpath/to/a/url.json\u003e \\\n  --headerFile \u003cpath/to/a/header.json\u003e  \\ # optional\n  --rateLimit 100 \\ # optional\n  --outputFile \u003cpath/to/an/output.json\u003e # optional\n```\n\n### Quickstart\n\n1. Install - see [Installation](#installation)\n2. Create a urlFile JSON - see [urlFile](#urlfile) - and setup up at least one target\n\nMinimal `urlFile` example:\n\n```json\n{\n  \"targets\": [\n    {\n      \"relativePath\": \"/some/relative/path\",\n      \"httpMethod\": \"GET\",\n      \"expectedStatusCode\": 200\n    }\n  ]\n}\n```\n\n3. execute `apijc`\n\n```sh\napijc \\\n  --baseDomain \"\u003chttp://your-base.domain\u003e\" \\\n  --newDomain \"\u003chttp://your-other.domain\u003e\" \\\n  --urlFile path/to/your/urlFile.json\n```\n\nNote: if `--rateLimit` is not passed to `apijc`, the default rate limit is 1 request per second.\n\n## Example output\n\n```sh\n$ apijc --baseDomain http://localhost:8080 \\\n  --newDomain http://localhost:8081 \\\n  --urlFile .testdata/urlfile_example.json \\\n  --rateLimit 1000\n\nStarting with rate limit: 1000.000000/second\n\n2023/12/06 22:09:35 Checking GET /v1/example\n2023/12/06 22:09:35 Success: GET /v1/example (checked 1 of 1 paths)\n\n2023/12/06 22:09:35 Checking GET /v1/{1-100}\n2023/12/06 22:09:36 Success: GET /v1/{1-100} (checked 100 of 100 paths)\n\n2023/12/06 22:09:36 Checking GET /v1/@1-3@\n2023/12/06 22:09:36 Success: GET /v1/@1-3@ (checked 3 of 3 paths)\n\n2023/12/06 22:09:36 Checking GET /v1/expected_jsonmissmatch\n2023/12/06 22:09:36 ERROR: GET /v1/expected_jsonmissmatch (checked 1 of 1 paths)\n\n2023/12/06 22:09:36 Checking POST /v1/example\n2023/12/06 22:09:36 Success: POST /v1/example (checked 1 of 1 paths)\n\n2023/12/06 22:09:36 Checking sequential group: First POST, then GET\n2023/12/06 22:09:36 Success: POST /v1/sequential_post (checked 1 of 1 paths)\n\n2023/12/06 22:09:36 Success: GET /v1/sequential_get (checked 1 of 1 paths)\n\n2023/12/06 22:09:36 Done. Checked 108 of 108 paths\n\n2023/12/06 22:09:36 Findings:\n2023/12/06 22:09:36 /v1/expected_jsonmissmatch\nError: JSON mismatch\nDiff: @ [\"foo\"]\n- \"baz\"\n+ \"bar\"\n2023/12/06 22:09:36 Finished - 1 findings\nexit status 1\n```\n\n## Configuration\n\n### CLI Flags\n\n| Flag       | Required | Description                                                                                                                                  | Default |\n| ---------- | -------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ------- |\n| baseDomain | yes      | The first domain to make all requests to                                                                                                     | -       |\n| newDomain  | yes      | The second domain to make all requests to                                                                                                    | -       |\n| urlFile    | yes      | Path to JSON file containing target URL paths, HTTP method, ...\u003cbr /\u003eSee [urlFile](#urlfile)                                                 | -       |\n| headerFile | no       | Path to JSON file containing global and/or per-domain header key-value pairs that will be set on each request. See [headerFile](#headerfile) | -       |\n| rateLimit  | no       | Requests per second (float).\u003cbr /\u003e See [rateLimit](#ratelimit)                                                                               | 1       |\n| outputFile | no       | Path to store findings in JSON format. See [outputFile](#outputfile)                                                                         | -       |\n\n### urlFile\n\nThe `urlFile` defines the relative paths that will be requested and compared on\nboth domains. It contains `targets` and/or `sequentialTargets`.\n\n#### targets\n\nStandalone requests are defined in the `targets` key of the `urlFile`. Each\ntarget will be requested on both, `baseDomain` and `newDomain`.\n\n#### sequentialTargets\n\nIn the `sequentialTargets` key chains of consecutive calls can be defined.\nExample: first make a POST request to create an entity, then\nmake a GET request to fetch the created entity.\n\nThe steps for a sequential target group are:\n\n1. make request to target 1 on `baseDomain`\n2. compare actual status code vs `expectedStatusCode`\n3. make request to target 1 on `newDomain`\n4. compare actual status code vs `expectedStatusCode`\n5. compare response bodies\n6. make request to target 2 on `baseDomain`\n7. compare actual status code vs `expectedStatusCode`\n8. make request to target 2 on `newDomain`\n9. compare actual status code vs `expectedStatusCode`\n10. compare response bodies\n\n#### Structure\n\n```json\n{\n  \"targets\": [\n      {\n        \"relativePath\": \"\u003crequired string, /a/relative/path/to/check/on/both/domains\u003e\",\n        \"httpMethod\": \"\u003cGET|POST|...\u003e\",\n        \"expectedStatusCode\": \u003crequired int; checked on both domains\u003e,\n        \"requestBody\": \"\u003coptional string, body to send to relativePath\u003e\",\n        \"requestBodyFile\": \"\u003coptional string, path to a file containing a JSON request body\u003e\",\n        \"requestHeaders\": { // optional\n          \"\u003cstring, header key\u003e\": \"\u003cstring, header value\u003e\"\n        },\n        \"patternPrefix\": \"\u003coptional string, a character to start expansion; default {\u003e\",\n        \"patternSuffix\": \"\u003coptional string, a character to stop expansion; default }\u003e\"\n      }\n    ],\n  \"sequentialTargets\": {\n    \"Some name for the sequence, example: Create Order, then fetch Order\": [\n      {\n        \"relativePath\": \"/first/path\",\n        \"httpMethod\": \"\u003cGET|POST|...\u003e\",\n        \"expectedStatusCode\": 201\n      },\n      {\n        \"relativePath\": \"/second/path\",\n        \"httpMethod\": \"\u003cGET|POST|...\u003e\",\n        \"expectedStatusCode\": 200\n      }\n    ]\n  }\n}\n```\n\n#### Path expansion\n\n`relativePath` can contain lists and/or ranges to quickly define multiple\ntargets at once.\u003cbr /\u003e\nExpansions are triggered for everything between a configurable `patternPrefix`\n(default: `{`) and a `patternSuffix` (default: `}`) on each target.\u003cbr /\u003e\nList items are separated by `,` (comma).\u003cbr /\u003e\nNumerical ranges can be defined by `-` (dash).\n\nExample:\n\n```json\n\"relativePath\": \"/foo/{bar,3-5}\"\n```\n\nThis will translate to requesting and checking 4 paths:\n\n- `/foo/bar` \u003c-- from list item `bar`\n- `/foo/3` \u003c-- from range `3-5`\n- `/foo/4` \u003c-- from range `3-5`\n- `/foo/5` \u003c-- from range `3-5`\n\nNote: a path can also define multiple expansions, like `/foo/{1-2}/bar/{a,b}`.\nThis path will result in 4 paths in total:\n\n- `/foo/1/bar/a`\n- `/foo/1/bar/b`\n- `/foo/2/bar/a`\n- `/foo/2/bar/b`\n\n#### requestBody vs requestBodyFile\n\nThe `urlFile` can contain two different exclusive keys to specify the request body to a target: `requestBody` and `requestBodyFile`.\n\n`requestBody` contains an escaped JSON string to be sent as the body.\n\nExample:\n\n```json\n\"requestBody\": \"{\\\"a\\\":\\\"b\\\"}\",\n```\n\n`requestBodyFile` contains a path to a JSON file containing the body to be sent\nfor the target. This is helpful if the request body to be sent is bigger and avoids escaping hell.\n\nExample:\n\n```json\n\"requestBodyFile\": \".testdata/request_body.json\"\n```\n\n#### urlFile Example\n\n```json\n{\n  \"targets\": [\n    {\n      \"relativePath\": \"/v1/example\",\n      \"httpMethod\": \"GET\",\n      \"expectedStatusCode\": 200\n    },\n    {\n      \"relativePath\": \"/v1/{1-100}\",\n      \"httpMethod\": \"GET\",\n      \"expectedStatusCode\": 200\n    },\n    {\n      \"relativePath\": \"/v1/example\",\n      \"httpMethod\": \"POST\",\n      \"expectedStatusCode\": 201,\n      \"requestBody\": \"{\\\"a\\\":\\\"b\\\"}\",\n      \"requestHeaders\": {\n        \"Content-Type\": \"application/json\"\n      },\n      \"patternPrefix\": \"{\",\n      \"patternSuffix\": \"}\"\n    },\n    {\n      \"relativePath\": \"/v1/post_with_body_file\",\n      \"httpMethod\": \"POST\",\n      \"expectedStatusCode\": 201,\n      \"requestBodyFile\": \".testdata/request_body.json\"\n    }\n  ],\n  \"sequentialTargets\": {\n    \"First POST, then GET\": [\n      {\n        \"relativePath\": \"/v1/sequential_post\",\n        \"httpMethod\": \"POST\",\n        \"expectedStatusCode\": 201,\n        \"requestBody\": \"{\\\"a\\\":\\\"b\\\"}\"\n      },\n      {\n        \"relativePath\": \"/v1/sequential_get\",\n        \"httpMethod\": \"GET\",\n        \"expectedStatusCode\": 200\n      }\n    ]\n  }\n}\n```\n\n### rateLimit\n\nSometimes it's necessary to limit the rate with which the tool makes requests to the configured domains.\nThe flag `--rateLimit` allows to configure the rate.\u003cbr/\u003e\nMust be `int` or float.\u003cbr/\u003e\nThe number defines the allowed requests per seconds.\n\nExamples:\n\n- `--rateLimit=1`: 1 request per second (default)\n- `--rateLimit=0.5`: 1 request per 2 seconds\n- `--rateLimit=10`: 10 requests per second\n\n### headerFile\n\nThe `headerFile` allows to define key-value pairs in the `global` key that will be set on each\nrequest, in addition to the static `requestHeaders` defined on each target in\nthe `urlFile`.\u003cbr/\u003e\n\nAdditionally, it is possible to set per-domain header key-value pairs that will\nbe set on each request to the particular domain (`baseDomain|newDomain`).\nThis is helpful for example if you need to set different `Authorization` headers per domain.\n\nNote: The `global`, `baseDomain` and `newDomain` keys are all optional.\n\n#### headerFile Example\n\n```sh\n# header.json\n{\n  \"global\": {\n    \"SomeHeaderName\": \"Value applied to all requests to both domains\"\n  },\n  \"baseDomain\": {\n    \"SomeHeaderName\": \"Value applied to all requests to BaseDomain\"\n  },\n  \"newDomain\": {\n    \"SomeHeaderName\": \"Value applied to all requests to NewDomain\"\n  }\n}\n```\n\n#### Precedence\n\nIf the `headerFile` and a target's `requestHeaders` contain duplicate header keys,\nthe target's `requestHeaders` value takes precedence.\n\n```\nheaderFile.global \u003c headerFile.\u003cnew|base\u003eDomain \u003c target.requestHeaders\n```\n\n### Output\n\n#### stdout\n\nIf the flag `--outputFile` is not passed, the findings are written to\nstdout, see [Example output](#example-output)\n\n#### outputFile\n\nVia `--outputFile` a path to a file can be passed. The findings will be written\nto this file instead of stdout.\n\n##### outputFile Example\n\n```sh\n# findings.json\n[\n  {\n    \"url\": \"/v1/expected_jsonmissmatch\",\n    \"error\": \"JSON mismatch\",\n    \"diff\": \"@ [\\\"foo\\\"]\\n- \\\"baz\\\"\\n+ \\\"bar\\\"\\n\"\n  }\n]\n```\n\n## Exit codes\n\nOn successful execution `apijc` exits with code `0`.\nOn any issue the exit code will be `\u003e 0`\n\n## TODOs\n\n- [x] read `requestBody` JSON from files\n- [ ] allow to skip the diff comparison for JSON response body fields on a target (e.g. `id`)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphux%2Fapijc","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fphux%2Fapijc","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fphux%2Fapijc/lists"}