Case Study: Moving an Old Client to a New Customer API With AfterAPI
This case uses the examples/customer-api folder from the code repository, so you can run every step yourself. The commands and output below come from a real run of aftermarket 0.3.0.
The situation
A customer service was rewritten, and its v1 API was switched off. One old client still calls v1, and it comes with its own test suite of twelve strict checks. Field names, extra fields, status codes and error messages all count.
The new API differs in almost every way the client can see:
- A lookup moved from
?id=7to/customers/7, and the customer is nested under"customer"with renamed fields. - Lists come wrapped in
{"data": [...]}. - Search moved to a query parameter on the list.
- A create answers
201where v1 said200, and a delete answers204with no body where v1 said200with{"deleted": true}. - Errors come as
{"error": {"code": ..., "message": ...}}.
Against v2 directly, every check fails:
$ go run ./examples/customer-api/client -base http://127.0.0.1:9002
FAIL get a customer
GET /api/v1/customer?id=7: status 404, want 200 (body {"error":{"code":"not_found","message":"no such endpoint"}})
FAIL missing customer is a 404 with a message
got {"error":{"code":"not_found","message":"no such endpoint"}}, want {"message":"customer not found"}
...
12 checks, 12 failed
That’s too much for a few small patch rules. AfterAPI takes the other approach: write down what each old endpoint was, in terms of the new one.
Five mappings
adapt.yaml has one entry per old endpoint. The create, for example, builds the v2 request body from the v1 one, maps the status codes, and rebuilds the old answer:
- id: create-customer
old: POST /api/v1/customers
new: POST /api/v2/customers
request:
body:
full_name: $.name
email: $.email
response:
status: {201: 200, 422: 400}
body:
customer_id: $.customer.id
name: $.customer.full_name
email: $.customer.email
error:
message: $.error.message
adapt check reads the whole file and sums up each mapping:
$ aftermarket adapt check --config examples/customer-api/adapt.yaml
examples/customer-api/adapt.yaml: 5 endpoint(s), max_body 1048576 bytes, upstream_timeout 30s
1. get-customer (line 11): GET /api/v1/customer?id={id} => GET /api/v2/customers/{id} (response body, error body, response headers)
2. delete-customer (line 24): DELETE /api/v1/customer?id={id} => DELETE /api/v2/customers/{id} (status 204->200, response body, error body)
3. search-customers (line 34): GET /api/v1/customers/search?q={q} => GET /api/v2/customers?search={q} (response body)
4. list-customers (line 44): GET /api/v1/customers => GET /api/v2/customers (response body)
5. create-customer (line 54): POST /api/v1/customers => POST /api/v2/customers (request body, status 201->200, 422->400, response body, error body)
ok
A dry run on the real client
Before changing any traffic, run adapt with --dry-run and point the client at it. The client still fails all twelve checks, since nothing is translated. The audit log is the useful part: it shows which mapping caught each call, and what it would have changed.
{"event":"would_change","request":1,"rule":"get-customer","phase":"request","method":"GET","path":"/api/v1/customer","query_keys":["id"],"actions":["rewrite path","rewrite query"]}
{"event":"would_change","request":1,"rule":"get-customer","phase":"response","method":"GET","path":"/api/v1/customer","query_keys":["id"],"status":404,"actions":["rebuild error body"],"not_applied":["remove header X-Api-Version (not present)"]}
Counted up, the dry run caught all ten requests the client sent: three lookups, a delete, two searches, two lists and two creates. All five mappings showed up. The suite makes twelve checks, but in a dry run nothing gets created, so the two checks that read back and delete the new customer stop before sending anything. A request no mapping catches would leave no line at all, and that’s the gap a dry run is for.
Live
$ go run ./examples/customer-api/client -base http://127.0.0.1:8080
...
PASS a deleted customer is gone
PASS deleting a missing customer is a 404 with a message
12 checks, 0 failed
The run left 24 lines in the audit log, one for each request and one for each answer. The create shows both sides:
{"event":"changed","request":7,"rule":"create-customer","phase":"request","method":"POST","path":"/api/v1/customers","actions":["rewrite path","rebuild body"]}
{"event":"changed","request":7,"rule":"create-customer","phase":"response","method":"POST","path":"/api/v1/customers","status":201,"actions":["status 201 -> 200","rebuild body"]}
The log never holds values. Path parameters stay out of it too, because adapt logs the endpoint’s pattern, not the path with the customer number in it.
In CI
The example ships with six fixture cases that cover a lookup, an error, a create, a delete, the search and a path no mapping touches:
$ aftermarket adapt test --config examples/customer-api/adapt.yaml --fixture examples/customer-api/fixture.yaml
PASS a v1 lookup reaches v2 and comes back in the v1 shape
PASS a v2 error becomes a v1 error
PASS create maps the body both ways and 201 becomes 200
PASS delete turns 204 into 200 with a body
PASS search unwraps the list
PASS paths no mapping covers pass through untouched
6 case(s), 0 failed
The repository’s own tests go one step further. They take each of the five mappings out in turn and run the client again, and something has to fail every time, which proves none of the five is dead weight. The release check case study shows how to prove a mapping like this on recorded traffic as well. The full format is in the AfterAPI reference.