Quick answer: An unmounted router is a router object that exists in the code but is never attached to the running app, so the routes it defines are unreachable. The scan reports routes and routers that are never mounted rather than treating every defined path as a live endpoint, keeping dead code out of the adopted contract. A resolved route definition and a reachable route are two different things.
Why defined routes can still be dead
It is easy to assume that a path with a handler is callable, but the handler only matters if a router is mounted on the app. A router left over from a refactor, behind a feature flag, or constructed but never wired in keeps all its routes unreachable even though the code looks complete.
That creates a gap between the code that exists and the surface that actually runs. A document that lists unmounted routes as live endpoints overstates the API and misleads anything built on the spec.
Detection vs reachability
| State | What the code shows | Can it be called? |
|---|---|---|
| Mounted, reachable | The router is attached to the app | Yes |
| Defined but unmounted | The router exists but is never wired | No |
| Unreachable route | The path sits behind an unmounted router | No |
| Missing route | No definition was resolved | No |
The scan follows the framework instance to see which routers are actually mounted, rather than assuming every router object it parses is live.
How the scan reports them
Routes and routers that are never mounted are reported as unreachable, so the finding stays visible instead of being quietly emitted as a normal endpoint.
- The AST resolves the router and its route definitions.
- Framework tracing checks whether the router is mounted.
- An unmounted router is reported with its routes unreachable.
- You decide whether to mount it, remove it, or keep it out of the contract.
This is the same logic that keeps a file with no incoming references from being treated as safe to delete. The structural signal is a cue for a decision, not a verdict by itself.
A concrete case is a codebase that keeps an admin router in a separate file. The router and its handlers are fully written, and a name scan would emit every admin path as a live endpoint. But the import that mounts it was removed during a cleanup, so the running app never receives those requests. The trace reports the router as unmounted and its routes as unreachable, and a runtime call confirms it with a 404. The code is not broken in isolation, but it is not part of the surface either.
What to do with an unmounted router
The response depends on why the router is not wired.
- If it is dead code from an old flow, remove it rather than documenting it.
- If it is behind a feature flag, note the condition before treating the routes as reachable.
- If it was forgotten, mount it and confirm the routes become reachable.
- If it is intentionally separate, keep it out of the adopted spec.
A successful scan still does not prove conformance, and static analysis can miss dynamic mounting. When a router is mounted through a path the trace does not resolve, verify it with a runtime check rather than assuming it is unmounted.
Powerduck keeps the reachability finding attached to the router rather than flattening defined and live routes together.
Frequently asked questions
Does a defined route always work? No. It is only callable when its router is mounted on the app. An unmounted router leaves its routes unreachable.
How does the scan tell a mounted router from an unmounted one? It traces the framework instance to see which router objects are attached, rather than relying on the route definitions alone.
Can dynamic mounting be missed? Yes. If a router is mounted through a path the static trace does not follow, verify reachability at runtime before treating it as dead code.
The bottom line
Keep route definition and route reachability separate. A router that is never mounted keeps its paths dead even when the handlers are complete, and the scan reports those routers instead of promoting them to live endpoints. Decide whether to mount, remove, or exclude them, and verify dynamically mounted routers another way. Treating every defined path as callable is how dead code enters a contract. Continue in the code-to-OpenAPI guide.
Top comments (0)