Use Cases
Each of these is a situation the tools were built for, with the smallest config that handles it. The case studies walk through twelve of them in full, with real commands and real output.
A field was renamed
The service now answers "amount" where your app reads "total". One AfterPatch rule puts the old name back:
rules:
- id: total-renamed
when: {path: /orders/*, status: 200, content_type: application/json}
response:
json:
- rename: {from: amount, to: total}
The key keeps its place in the object, and every other byte of the answer stays as it was.
The service now wants a header
A new version answers 400 unless the request carries a version header, and the old client doesn’t send one:
rules:
- id: send-api-version
when: {path: /api/**}
request:
set_headers: {X-Api-Version: "2026-10"}
An endpoint moved
/v1/rate is gone and the same thing now lives at /v2/rate. The query string goes along exactly as the client wrote it:
rules:
- id: rate-moved
when: {method: GET, path: /v1/rate}
request:
rewrite_path: {from: "^/v1/rate$", to: "/v2/rate"}
The exchange rate case study combines this with the two cases above.
Something leaks that shouldn’t
An upstream started sending a debug header and an internal field to every client:
rules:
- id: strip-internals
when: {content_type: application/json}
response:
remove_headers: [X-Debug-Trace]
json:
- delete: {path: internal}
A whole API version was retired
When the paths, the body shapes and the status codes all changed, AfterAPI is easier to read than a stack of patch rules. You write each old endpoint in terms of the new one:
endpoints:
- id: get-customer
old: GET /api/v1/customer?id={id}
new: GET /api/v2/customers/{id}
response:
body:
customer_id: $.customer.id
name: $.customer.full_name
error:
message: $.error.message
The status codes changed
The new API answers 201 to a create and 204 to a delete, where the old client expects 200 both times. AfterAPI maps the codes and can add the body the old client looks for:
endpoints:
- id: delete-customer
old: DELETE /api/v1/customer?id={id}
new: DELETE /api/v2/customers/{id}
response:
status: {204: 200}
body: {deleted: true}
The customer API case study moves a whole client this way.
Try a fix on live traffic first
Every run command takes --dry-run. Traffic passes through untouched, and the audit log says what each rule would have changed:
aftermarket patch run --config rules.yaml --upstream http://127.0.0.1:9000 --dry-run
Check a new release before it ships
Record what the current version answers, then send the same requests to both versions:
aftermarket record --upstream http://old:9000 --out traffic.yaml
aftermarket replay --fixture traffic.yaml --a http://old:9000 --b http://new:9000 --ignore created_at
Every difference is listed by field. Key order and number formatting don’t count, and only GET and HEAD requests are sent unless you add --include-writes. The release check case study finds a renamed field this way and proves the repair.
Try the new version on live traffic before the switch
A recording only holds the requests you thought to send. Shadow mode serves every request from the current version and sends a copy to the new one in the background, then lists every difference:
aftermarket shadow --primary http://old:9000 --candidate http://new:9000 \
--ignore updated_at --sample 0.1 --report shadow-report.md
Clients only ever get the current version’s answers, and a slow or broken candidate can’t hold them up. Run the current version against a copy of itself first to find the fields that differ on their own. The Datasette case study does that before comparing 0.65.5 with the 1.0 alpha, the JSON Server case study copies writes to a candidate with its own data file, and the load case study shows what the report says when the candidate can’t keep up. The shadow mode recipes have short setups for each of these.
Turn real traffic into regression tests
A recording is an ordinary fixture file, with credentials saved as REDACTED. Run it against your rules in CI, and the build fails if a rule starts changing answers it shouldn’t:
aftermarket patch test --config rules.yaml --fixture traffic.yaml
Compare a service with what it used to answer
Without --b, replay compares one service with the answers in the recording. That fits a migration, a dependency upgrade or a config change, where the before picture is only available as a recording:
aftermarket replay --fixture traffic.yaml --a http://127.0.0.1:9000 --ignore created_at
Every upgrade overwrites your config changes
A vendor’s config.json gets replaced on each upgrade, so your edits to it keep disappearing. Keep them in an overlay instead, each guarded by the value it expects:
version: 1
base: /opt/vendor/config.json
changes:
- set: {path: server.timeout, value: 120, expect: 30}
- set: {path: telemetry.enabled, value: false, expect: true}
aftermarket config apply --overlay overlay.yaml --out /etc/app/config.json writes the result and never touches the vendor’s file. When an upgrade changes the default timeout, apply stops at that line instead of overriding it. The same works for YAML, TOML and .env files. In a TOML file only the changed lines differ from the vendor’s, comments included.
An old app changes data and tells no one
The app writes order status to a file, or straight into its SQLite database, and has no webhooks. AfterHook checks both and posts a signed event when something changes:
state: hook-state.json
webhooks:
- id: ops
url: https://hooks.example.com/orders
secret_env: HOOK_SECRET
sources:
- id: order-status
file: {path: status.json}
ignore: [updated_at]
- id: orders-table
command:
run: [sqlite3, -json, app.db, "select id, status, total from orders order by id"]
interval: 15s
A new row arrives as one added row, a paid order as one changed field. A timestamp that moves on its own sends nothing, and while the receiver is down the events wait in the state file. The legacy app case study runs this with a real receiver.
An old app has no login at all
An admin panel from years ago trusts every request, and a debug page prints a password. AfterGuard goes in front, and the panel moves to 127.0.0.1 where only AfterGuard can reach it:
default: deny
clients:
- id: admin
basic_user: admin
basic_password_sha256: 729d88640e95cc2e6a8a188e992f87a3502602dc7ed49543d9454ea1bc0fc429
rules:
- id: no-debug
when: {path: /debug/**}
deny: 404
- id: admin-office
when: {path: /admin/**}
from: [192.168.0.0/16]
client: [admin]
allow: true
Five wrong passwords from one address and it waits a minute. A request built to be read two ways gets 400 and never reaches the panel. The legacy admin case study runs this against a stand-in panel.
The database changed and the old reports broke
The app’s 2.0 release renamed customers to accounts, split the name in two and keeps totals in cents. The reporting scripts still ask for customers and orders. A mapping says how the old tables read from the new ones:
version: 1
old: v1/shop.sql
new: v2/shop.sql
views:
- old: customers
from: accounts
columns:
name: first_name || ' ' || last_name
fax: null
created: created_at
where: deleted_at IS NULL
- old: orders
from: purchases
columns:
customer_id: account_id
total: total_cents / 100.0
status: state
placed: placed_at
aftermarket schema test --map map.yaml --queries reports.sql runs every old report on both databases and compares the rows exactly, and aftermarket schema compat --map map.yaml --out compat.sql writes the views for you to apply. The shop case study shows what the test catches when the mapping is wrong.
The app only hands its data over as files
The old shop app writes an orders export into a folder every night and keeps its stock in SQLite. The new dashboard wants an API. AfterBridge serves the files as one:
version: 1
sources:
- id: orders
csv: {folder: exports, pattern: "orders-*.csv"}
key: id
columns: {id: integer, total: number, paid: boolean, placed: date}
exclude: [internal_note]
endpoints:
- path: /orders
source: orders
filters: [paid, placed]
- path: /orders/{id}
source: orders
aftermarket bridge serve --config bridge.yaml answers /orders?paid=false and /orders/1003 from the newest export, and moves to the next night’s file once it is complete. The exports case study shows a half-written and a broken export being kept out.
The full formats are in the AfterPatch, AfterAPI, AfterTest and AfterDiff, AfterConfig, AfterHook, AfterGuard, AfterSchema and AfterBridge references.