How would you choose a route registration convention for a service with hundreds of endpoints owned by several teams?
answer
- declaration site decides who reviews
- ownership boundaries versus one central file
- reserved prefixes, collision fails at startup
- hooks on the group make routes safe by default
- printable inventory diffed in review
basics
~20 sOptimise for ownership and review, not typing. Compose per-module route trees at reserved prefixes, attach cross-cutting hooks at the group so new routes are safe by default, and keep the effective inventory printable and diffable.
solid answer
~50 sAt this size the questions stop being about syntax. Decide **where a route is declared relative to who owns it**: one central table gives a single reviewable surface but becomes a merge-conflict magnet edited by everyone, while per-module trees mounted at reserved prefixes align declarations with ownership at the cost of no single screen showing the whole service. Recover that screen by generating the **effective route inventory** as a build artefact and diffing it, so new exposure is visible even when it arrives as a new file. Attach authentication and other cross-cutting hooks at the **group level**, so an endpoint added tomorrow inherits them by omission rather than by memory — per-route opt-in eventually ships something unprotected. Make conflicting or shadowing registrations fail at startup. Weigh the migration cost honestly: changing style later touches every endpoint, so leave a small explicit escape hatch for irregular routes.
go deeper
Focus first on following whatever convention the codebase already uses, and on knowing where routes are declared. The governance side is not expected of you yet.
Be able to explain why declaration location affects merge conflicts and review, and why shared hooks belong on a group rather than being repeated per route.
Argue the safe-by-default position concretely, and set up the mechanics: reserved prefixes, startup failure on collisions, and a generated route inventory that review can diff.
Own the tradeoff between team autonomy and a single reviewable surface, and price the migration: a registration convention touches every endpoint, so it should be chosen for the org it will outlive.
With hundreds of endpoints and several owning teams, route registration stops being a style preference and becomes an organisational mechanism. The declaration site decides who reviews a change, where merge conflicts land, whether a new endpoint is protected by default, and how anyone answers "what does this service expose". There is no single right answer, which is why this is a judgment question; what a strong answer shows is the axes and the costs on each. ## What you are actually choosing - **Where the declaration lives relative to ownership.** Routes declared inside the module that owns them put the change in front of the right reviewers. Routes declared centrally put every change in front of one file that everybody edits. - **How exposure becomes visible.** Adding a public endpoint is a security-relevant event; the convention decides whether it shows up as an obvious diff line or as one more file in a tree. - **What a new route inherits by default.** If cross-cutting behaviour is attached per route, the default for a forgotten route is *nothing*. If it is attached at the enclosing group, the default is the group's policy. - **How mistakes surface.** Conflicts, shadowed paths and unregistered handlers can fail at boot or be discovered in production; that choice is largely yours, through startup checks. - **How much a team can change alone.** A convention that lets a module move its own endpoints without touching shared files is faster to work in, but it also lets a prefix change ship without anyone outside the team noticing. ## Composition: one table or many | Approach | Strength | Cost | |---|---|---| | One central table | The whole surface on one screen; ordering is explicit and reviewable | Constant merge conflicts; review load on people who do not own the code; the file grows past readability | | Per-module trees mounted at reserved prefixes | Declarations sit with owners; a module can be moved or removed as a unit | No single screen; prefix collisions become possible; each module can drift stylistically | | Free-for-all, each team choosing a style | Nothing to negotiate up front | The inventory can only be assembled by hand; onboarding cost paid forever | The usual sound answer is the middle row plus tooling that buys back the first row's advantage. ## Prefix ownership If modules mount themselves, the prefix space needs an owner. In practice: 1. **Reserve prefixes explicitly** in one list, so two modules cannot claim overlapping bases by accident. 2. **Check that list at startup** and refuse to start on a collision — do not assume the framework detects every overlap, especially through wildcard segments. 3. **Keep the root shallow.** Endpoints registered directly at the root are the ones that later block a module from taking a prefix it needs. 4. **Treat a prefix move as a contract change**, because it is one for every client, and reverse generation only fixes your own links. ## Safe by default The security argument usually decides the debate. Attach authentication, authorization and similar cross-cutting hooks to the **group or mounted tree**, so a route added inside inherits them without anyone remembering; then the dangerous act — exposing something publicly — requires writing an explicit exception, which is exactly the change a reviewer should see. Per-route opt-in has the opposite default: the omission is silent and ships. ## Inventory and detection Whatever the style, generate the **effective inventory** — every registered method and fully resolved template with its owning module — as a build artefact, and diff it in review. It answers the exposure question in one place, catches a route that silently failed to register, catches an accidental prefix change, and does so without forcing every team into one declaration style. Pair it with startup failure on duplicate or shadowing registrations so the feedback arrives before deployment. ## The migration cost Changing registration style later touches every endpoint and every team at once, so the decision has a long half-life. Two hedges make it survivable: keep a small **explicit escape hatch** for routes that must be built from configuration or that do not fit the convention, and avoid conventions that bind a URL to something you refactor often, since that turns ordinary cleanup into a public contract change. A convention that the teams accept and that makes exposure visible beats a theoretically tidier one that everybody works around.
- Why is attaching authorization hooks per route riskier than attaching them to the enclosing group?Because the failure mode is omission, and omission is silent. A per-route scheme protects only the routes someone remembered, so the first forgotten endpoint is public with no error anywhere. Group attachment inverts the default: the route inherits the policy, and making something public requires an explicit, reviewable exception.
- What does a diffable route inventory catch that code review alone does not?New or changed exposure that does not look like a route change — a moved file under a layout convention, a marker added to an existing type, a group prefix edited one level up, or a handler that silently stopped registering. The inventory shows the resolved method-and-template set, so any of those appears as an added, removed or renamed line.
- How would you migrate a large codebase from one registration style to another?Incrementally, with the inventory as the invariant: snapshot the effective routes, move one module at a time, and require the generated inventory to be unchanged except for lines you intend. Keep both styles registering into the same table during the transition, and finish by removing the old path so a third style never accumulates.
saying these in an interview costs you the question
- Picks a registration style by personal taste without naming its cost
- Lets every team invent prefixes with no reserved namespace
- Makes security hooks opt-in per route, so a forgotten route ships open
- Assumes one central table scales to many teams without merge pain
- Treats a later change of registration style as a cheap refactor
- Has no way to answer what the service currently exposes