apic
A small CLI for git-able API contracts. Each endpoint is a plain JSON file
that lives in your repository, so contracts are diffable, reviewable in pull
requests, and versioned alongside the code they describe. apic discovers,
renders, and scaffolds those files.`
https://github.com/user-attachments/assets/89b5fb4b-7942-49a1-9bee-41308d234236
Why
Mainstream API tools like Postman and apidoc gate collaboration behind a
paywall — you pay per team member to share workspaces, and seats add up fast as
a team grows. apic takes a different approach: contracts are plain JSON files
in your repository, so your existing git workflow is the collaboration
layer. No seats, no separate accounts — if someone can clone the repo, they
can read, edit, and review contracts.
That means contracts are:
- Free to collaborate on — sharing is
git push/git pull, not a billing tier. Everyone with repo access already has full access. - Version-controlled — contracts change in the same commit as the code, with full history and blame.
- Reviewable — a contract change is a readable diff in a pull request, reviewed by the same people on the same platform as the code.
- Readable —
apic readrenders a contract as a clean, colorized table in the terminal instead of raw JSON.
Install
crates.io (recommended) — installs the apic command:
From source (requires a Rust toolchain, 1.88+):
To run without installing, use cargo run -- <args> from the project directory.
Prebuilt binaries — grab the archive for your platform from the
latest release, verify the
.sha256 checksum, extract, and put apic on your PATH. Builds are provided
for Linux (x86_64, aarch64), macOS (Intel, Apple Silicon), and Windows (x86_64).
Quick start
# 1. Initialize a project in the current directory (creates .apic/config.toml)
# 2. Point apic at the folder that holds your contract files
# 3. Scaffold a new contract from a template (opens it in your editor)
# 4. List and read contracts
Commands
apic init [--set-dir <dir>]
Initializes an .apic project in the current directory by creating
.apic/config.toml. The optional --set-dir records which directory contract
files are scanned from (defaults to the current directory).
apic config [--set-dir <dir>]
Updates project configuration.
--set-dir <dir>— change the working directory that contracts are scanned from (must exist).
apic create -f <filename> [-e <editor>]
Creates a new contract from the project template (.apic/template.json,
falling back to the built-in default) and opens it in your editor. A relative
path is resolved against the configured working directory. apic refuses to
overwrite an existing file.
Editor resolution order: --editor flag → $VISUAL → $EDITOR → vi. The
-e/--editor flag picks the editor for a single invocation (e.g.
apic create -f auth/login.json -e nano) and the value may include arguments.
GUI editors need their wait flag (code --wait, subl -w) so apic waits for
the file to be saved.
apic list [--filter <query>] [--absolute <true|false>]
Lists discovered .json contract files under the working directory.
--filter <query>— show only contracts whose path fuzzy-matches the query, best match first (e.g.apic list --filter user).--absolute <true|false>— print absolute paths or paths relative to the working directory (false, the default).
apic read -f <filename> [-s <status>]
Renders a contract as formatted tables. -s <status> filters the response
section to a single HTTP status code.
<filename> is resolved flexibly — an exact match wins, then fuzzy:
- a path relative to the working directory —
user/user.json - the same without the
.jsonextension —user/user,auth/login - a fuzzy fragment —
user,logn
By default each schema table is followed by its example payload (when the
contract provides one), labeled Example:, so structure and a concrete
payload read together. With --example (or -e) the schema tables are
skipped entirely and only the raw JSON payloads print — the compact
copy-paste view:
REQUEST
{
"username": "rizukirr",
"password": "123qweA@"
}
RESPONSE 200 — Successful login
{
"status": 200,
"message": "Login successful",
"data": { "access_token": "..." }
}
apic open (-f <filename> | --template) [-e <editor>]
Resolves <filename> exactly like read (path, extensionless, or fuzzy) and
opens the matching contract in your editor — the same editor resolution as
apic create, including the -e/--editor flag.
Pass --template instead of -f to edit the project template
(.apic/template.json) that apic create scaffolds from; it is seeded from
the built-in default first if it does not exist yet. --template and -f are
mutually exclusive, and exactly one is required.
Output is colorized when stdout is a terminal and plain when piped, so it stays clean in scripts. Contract strings are sanitized before display, so a file from an untrusted source cannot inject terminal escape sequences.
apic remove -f <filename>
Resolves <filename> exactly like read/open (path, extensionless, or
fuzzy, prompting to pick when ambiguous) and deletes the matching contract
file. On an interactive terminal it asks Remove <path>? [y/N] first and only
deletes on y/yes; when stdin/stdout is not a terminal (scripts) it removes
without prompting.
apic validate [-f <filename>]
Checks that contracts parse and conform to the schema. With no -f, every
contract under the working directory is checked; with -f <name> only the best
fuzzy match is. Prints ok/FAIL per file with the parse error (line and
column) for failures, and exits non-zero if any contract is invalid — so it
drops straight into a CI step or pre-commit hook.
ok auth/login.json
FAIL user/user.json: EOF while parsing an object at line 12 column 1
2 passed, 1 failed
Security
apic treats contract files and paths as untrusted, so it is safe to run
against contracts from any source:
- Terminal-escape safe — all file-derived strings (contract fields, file names) are stripped of control characters before printing.
- Path-confined —
apic createrefuses paths that escape the working directory via..or an absolute path elsewhere. - Bounded — contract files larger than 5 MiB are rejected before reading, and pathologically nested JSON is rejected rather than overflowing the stack.
Contract format
A contract is a single JSON object describing one endpoint. See
src/templates/contract.json for the full
template that apic create writes.
apic init writes a starter template to .apic/template.json. Edit it to set
a project-wide convention — for example a standing device-id header — and
every apic create reuses it. The file is never overwritten once it exists; if
it is missing or malformed, apic create falls back to the built-in default.
Both schema (field-level detail, rendered as tables) and example (a raw
JSON payload) are optional in the request and in each response — early-stage
contracts often start with just an example, formal ones with just a schema.
The default view shows the example beneath its schema table (or alone when
there is no schema), and read --example shows only the payloads.
Fields
| Field | Required | Description |
|---|---|---|
name |
yes | Endpoint name. |
description |
no | Short description of the endpoint. |
method |
yes | HTTP method (GET, POST, …). |
url |
yes | Request URL, broken into parts (see below). |
headers |
yes | Array of headers (name, value). |
request |
no | Request body: { "schema": [fields], "example": <raw JSON> } — both parts optional. |
responses |
yes | Array of responses (code, description, optional schema, optional example). |
The url object has:
| Field | Required | Description |
|---|---|---|
protocol |
yes | URL scheme, e.g. http or https. |
host |
yes | Host, e.g. api.example.com. |
path |
no | Path segments as an array, e.g. ["auth", "login"]. |
query |
no | Array of query parameters (name, value, description, required). |
variable |
no | Array of path variables (name, optional type — defaults to string, description). |
A field (in the request schema and response schema) has:
| Field | Description |
|---|---|
name |
Field name. |
type |
Data type (string, int, file, object, …). |
default |
Default value as a string, or null. |
description |
Field description. |
required |
Whether the field is required. |
accept |
Allowed MIME types for file fields, e.g. "image/png, image/jpeg". Request only; omit for ordinary fields. |
properties |
Nested fields (for object types), or null. Response schema only. |
Multipart / file uploads
For multipart/form-data endpoints, declare the encoding in the
Content-Type header as usual and use "type": "file" for file parts. The
optional accept field documents which MIME types the part allows, and
apic read shows it in an extra ACCEPT column:
REQUEST
NAME TYPE REQ ACCEPT DESCRIPTION
avatar file ✓ image/png, image/jpeg Avatar image, max 2MB
caption string Optional caption
Configuration
apic init writes .apic/config.toml:
= "apic"
= "0.1.0"
[]
= "api-contract"
working_dir is stored relative to the project root, so .apic/config.toml
is safe to commit and share — it resolves correctly on any clone. apic
locates the project by walking up from the current directory to find the
.apic directory, so commands work from anywhere inside the project tree.
License
Licensed under the MIT License.