Turn any REST API into the shape of another

thatxliner1 pts0 comments

Shotgun — make one API answer to another API's shape

OpenAPI → OpenAPI translation proxy<br>Turn any REST API into the shape of another.

Shotgun is a reverse proxy generator for OpenAPI specs.

Get started

$cargo install shotgun-proxycopy

OpenAPI 3.0 / 3.1 + Swagger 2.0<br>Deterministic matching<br>Rust + axum

The idea

APIs in the same domain already share most of their shape.

Git forges, payment processors, and CMS platforms usually share 60 to 80 percent of their structure.<br>Shotgun maps the obvious parts and leaves you a checklist for the rest.

01<br>Client thinks it's calling

GET /repos/{owner}/{repo}/issues/{issue_number}

gh, Renovate, and CI scripts run unmodified.

──▶

02<br>Shotgun rewrites

path_params · renames · defaults · drops

Driven by mappings.toml. Nothing guessed at runtime.

──▶

03<br>Upstream gets

GET /api/v1/repos/{owner}/{repo}/issues/{index}

The response comes back in the shape the client expected.

Quick start

Two commands and one edit get you a proxy.

Diff, review, serve. When a spec changes, re-diff without losing your edits.

01

Diff the specs

shotgun init matches endpoints and fields, then writes a mappings.toml. It leaves blank anything it can't resolve.

02

Fill in the gaps

Anything left as target = "" is your todo list. Mark what you touch edited = true to protect it from sync.

03

Serve it

Point --target-url at the upstream. Clients keep speaking the source API's shape.

terminal<br>copy

# 1. Diff two specs and generate a mapping file<br>shotgun init --source github-api.json --target forgejo-api.json --output mappings.toml

# 2. Review mappings.toml, fill in anything left as target = ""

# 3. Run the proxy<br>shotgun serve --mappings mappings.toml --target-url https://forgejo.example.com

# 4. Clients call Shotgun in the GitHub shape; it translates on the fly<br>curl http://localhost:8080/repos/owner/repo

# When specs change, re-diff without losing your edits.<br>shotgun sync --source github-api-v2.json --target forgejo-api-v2.json --mappings mappings.toml

# Check a mapping file for problems<br>shotgun validate --mappings mappings.toml

Matching<br>Shotgun only maps what it can prove.

It guesses nothing at request time. When it can't prove a match, it<br>leaves the entry unmapped and says so.

/Endpoints

Shotgun matches on path and HTTP method, ignoring {param} names. If that misses, it tries operationId. Anything left over stays unmapped.

=Fields

A field maps when the name matches exactly and the type fits. If the types clash, Shotgun flags it and forces nothing.

↺Renames

Shotgun never writes one. You write every rename, then mark it edited = true so sync leaves it alone.

+Defaults and drops

A field that only one side has becomes a default holding a zero value, or a drop that clients never see.

⌥Nested schemas

Shotgun diffs a User inside a Repository once. It reuses that map every time the type shows up, through a named [[schemas]] entry.

»Pagination

Shotgun remaps query params between the two styles. It also rewrites Link header URLs to point back at the proxy.

mappings.toml<br>One file you can read in a diff.

The whole translation lives in one TOML file. You can review it, version it, and mark<br>entries edited = true so a later sync skips them.

[settings]<br># prepended to every target path<br>target_base_path = "/api/v1"<br># stripped from the request before matching<br>source_base_path = "/api/v3"<br># "reject" (501) or "passthrough"<br>unmapped_endpoint_behavior = "reject"<br># "passthrough" / "drop" / "drop_unknown"<br>unmapped_field_behavior = "passthrough"

[settings.pagination]<br>source_style = "link_header"<br>target_style = "link_header"<br>rewrite_link_urls = true

[settings.pagination.param_map]<br>per_page = "limit"

[[endpoints]]<br>source = "GET /repos/{owner}/{repo}/issues/{issue_number}"<br>target = "GET /repos/{owner}/{repo}/issues/{index}"<br>edited = true # sync will not overwrite this

[endpoints.path_params]<br>issue_number = "index"

[endpoints.response.renames]<br>number = "index" # humans write these, never the diff

[endpoints.response.defaults]<br>draft = false # source-only field, synthesized<br>labels = []

[endpoints.response]<br>drops = ["due_date"] # target-only, hidden from clients

[[endpoints.response.nested]]<br>path = "user"<br>schema_map = "User" # reuse a [[schemas]] entry

[[schemas]]<br>name = "User"<br>edited = true

[schemas.renames]<br>login = "username"

[schemas.defaults]<br>site_admin = false

# No target equivalent exists; returns 501 at runtime<br>[[endpoints]]<br>source = "POST /repos/{owner}/{repo}/issues/{issue_number}/lock"<br>target = ""<br>note = "Forgejo has no issue locking API"

# Same name, incompatible types; flagged, left untouched<br>[endpoints.response.type_conflicts]<br>reactions = "object vs array"

Flagship use case

Anvil points GitHub tooling at Forgejo.

Anvil is a curated mappings.toml on top of this proxy. It lets GitHub tools like<br>gh, Renovate, and CI scripts talk to a self-hosted Forgejo instance with no<br>client-side changes.

It drives Shotgun's work against real specs, not toy ones.

View Anvil...

shotgun mappings target toml endpoints shape

Related Articles