DEV Community

Lucca Gabriel
Lucca Gabriel

Posted on

How to find every caller of a route across 20 repos, and keep the answer true

At my day job we have a dozen repositories: two web front ends, two Flutter apps, a handful of Express APIs and some RabbitMQ workers. One question kept coming up in code review: who calls PUT /devices/:id? Nobody could answer it without grepping every repo, and the architecture doc was two years out of date.

So I built a scanner that answers it from the code, and kept going until it drew the whole map. This post is about the parts that turned out to be hard, and how I check that the answers are right.

The OpenTelemetry demo mapped by Kuuhaku: 23 services and every call between them

Why grep is not enough

Searching for /devices/ finds some callers and misses most of them:

  • The host lives somewhere else. axios.create({ baseURL: process.env.API_URL }) in one file, api.put(`/devices/${id}`) in another, and API_URL=http://devices-api:3000 in a .env or a docker-compose.yml. The call only means something once you join all three.
  • Clients are wrapped. Real code calls an http(method, url) helper, a generated gRPC stub or a Gateway class, rarely fetch directly.
  • Routes are mounted. The handler says router.put("/:id"); the /devices prefix is added two files away with app.use("/devices", router), and a proxy (nginx, Envoy, Spring Cloud Gateway) may add another hop in front.
  • Paths are templates on one side and patterns on the other. The caller builds `/devices/${device.id}`; the route says /devices/:id, or {id}, or <int:id>.

Matching calls to routes

The scanner is static analysis (tree-sitter for JavaScript and TypeScript, pattern readers per framework for the other languages, no LLM). For each repo it:

  1. finds the routes it serves, with their full mounted path;
  2. finds the call sites: HTTP clients, gRPC stubs, queue publishers and consumers, database connections;
  3. works out where each call goes: the base URL from env files, compose files, constants and proxy config, then the path, normalised so /devices/${id} meets /devices/:id.

Then every call is matched against the routes of the service its host points at.

Evidence instead of percentages

Every connection on the map says why it is there. Real examples from the OpenTelemetry demo:

  • frontend → shipping, HTTP, config: SHIPPING_ADDR = http://shipping:50050 in compose.yaml, read in the same file.
  • checkout → cart, gRPC, config: CART_ADDR = cart:7070 in compose.yaml.
  • checkout → accounting, queue, inferred: topic orders, published at main.go:671, consumed at Consumer.cs:46.

There are four tiers: config (the address is written down somewhere), inferred (the path matches and nothing contradicts it), declared (a person confirmed it) and observed (seen at runtime, on the roadmap). No "87% confident". When the scanner can't settle something, like a base URL assigned at runtime, it goes to a review inbox instead of becoming a guessed arrow.

How I know it's right

I keep a benchmark of 40 public systems (the OpenTelemetry demo, Online Boutique, Train Ticket, eShop, Sock Shop, Spring PetClinic microservices and more) with hand-checked answer keys. Every scanner change runs the whole set, and a change that lowers the score doesn't ship. Today it's F1 97 on connections, precision 99 and recall 96.

That number is after fixing what those systems taught. The first run on 14 systems it had never seen scored F1 79, and every fix had to be a general rule, never a special case for one repo. New systems get scored once before any fix they prompt.

What the map is for

Once you have the graph, a few things get easy:

  • A pull-request check: "this PR removes PUT /devices/:id, which web calls at src/api.js:42".
  • An MCP server, so Claude Code or Cursor can ask what depends on a route before editing it: claude mcp add kuuhaku -- npx kuuhaku mcp.
  • Exports to Mermaid, C4 (Structurizr), Backstage and a PDF report.

Try it

Nine public systems are mapped and open without an account. The OpenTelemetry demo has 23 services in 10 languages and 30 connections, and against its answer key the scanner finds 27 of 28 and invents none. All nine are at kuuhaku.dev/maps. To map your own repos, the CLI runs on your machine and only the graph leaves it.

It's called Kuuhaku (空白, "blank": it fills in the blanks of your architecture), and it's free in early access until November 14: kuuhaku.dev. I'd love to hear where it gets your stack wrong.

Top comments (0)