skip to content

Your team keeps adding in-app endpoints that the pages' own server code could call directly. How do you decide which deserve to exist?

level: principalimportance: nice to knowfreq 37%

answer

  1. watch for a second API growing inside
  2. one module owns the rule
  3. the handler is a thin edge
  4. every endpoint names its audience
  5. count the calls the app makes to itself

basics

~20 s

Make each endpoint name an outside caller or a non-document response; everything else stays a server-side module the render calls directly. Keep the rule in that module so an endpoint is only ever a thin edge over it.

solid answer

~50 s

The risk is drift: a second API growing inside the frontend deployment, one reasonable addition at a time, until the app calls itself over HTTP for work the same process could do in a function call. My policy has four parts. **One module owns the rule** - validation, authorisation, the write - with a typed signature; nothing important lives in a handler. **An endpoint is a thin edge** over that module: parse, call, map the result. **Each endpoint records its audience**, and if the honest answer is "our own pages", it is a deletion candidate rather than a documentation candidate. **Server-side code never calls its own deployment over HTTP** - that is the smell that fails review. I keep an inventory with owner, audience and last-called date, because the asymmetry between minutes to add a URL and a deprecation window to withdraw one is what justifies the higher bar.

go deeper

for a junior

Notice the pattern rather than the policy: if the only caller of an endpoint is your own page, the work could probably have happened during the render instead.

for a middle

Be able to restructure one case - pull the rule out of a handler into a shared server module, then have both the render and the endpoint call it - and explain what that removes.

for a senior

Argue the operational cost: duplicated validation, extra failure modes, added latency, and a surface that has to be authorised and rate-limited endpoint by endpoint.

for a principal

Own the asymmetry between adding and withdrawing a URL, set the review bar in proportion to the stakes, and decide when the behaviour belongs to the service that owns the data instead.

The failure mode is not one bad endpoint. It is **drift**: a second API growing inside the frontend deployment, one reasonable-looking addition at a time, until the app is calling itself over HTTP for work the same process could do with a function call, and the team is maintaining a contract surface nobody designed. ## Why it happens Each individual step is defensible. Client-side interaction needs data after the first render. A developer already knows how to write a handler. A shared module would need a small refactor to be callable from both places, and the endpoint would not. Over a year that produces dozens of URLs, each slightly different from the last, none reviewed as an interface, most called only by the app's own pages. ## The policy worth defending 1. **One module owns the rule.** Business behaviour — the validation, the authorisation decision, the write, the invariants — lives in a server-side module with a typed signature. Nothing important lives in a handler. 2. **An endpoint is a thin edge over that module.** It parses the request, calls the module, maps the result to a response. A handler longer than that means the rule has leaked into the transport. 3. **Each endpoint records an audience.** Who, outside this deployment, calls it? If the honest answer is "our own pages", it is a candidate for deletion rather than a candidate for documentation. 4. **The render calls the module, never the URL.** Server-side code calling its own deployment over HTTP is the smell that should fail review outright. 5. **Removal has a cost, so addition needs a reason.** Adding a URL takes minutes; withdrawing one takes a deprecation window and a conversation with whoever depends on it. That asymmetry belongs in the review bar. ## The judgment calls the policy does not settle | Question | What pushes one way | What pushes the other | |---|---|---| | Should the service that owns the data expose this instead? | the rules belong to it, and a copy here will drift from them | the frontend needs a shape or aggregation no other consumer wants | | Endpoint, or a typed server-call mechanism where one exists? | the typed mechanism is refactorable and not a public contract | an outside caller exists, so a stable URL is required anyway | | One coarse endpoint or several specific ones? | fewer contracts to keep, fewer authorisation decisions | a coarse one accretes parameters until nobody can review it | | Move it out of the frontend deployment? | it needs a runtime, duration or scaling shape the pages lack | it is small, well bounded and genuinely presentation-specific | ## Making it stick without becoming the endpoint police - **Put the test in the template.** If the scaffold for a new handler asks for the audience in a required field, the question gets asked when it is cheapest to answer. - **Make the compliant path the easy one.** Most drift is refactor avoidance. If calling a shared server module from a render is as convenient as writing a handler, most of the pressure disappears. - **Keep an inventory.** A list of the deployment's endpoints with owner, audience and last-called date turns "can we delete this?" from archaeology into a lookup. - **Review deletions, not only additions.** An endpoint with no external caller and no traffic for a quarter should be retired on a schedule rather than on somebody's initiative. - **Track the loopback count.** The number of places where the application makes an HTTP request to itself is one number that describes the health of this boundary, and it should trend toward zero. ## The tradeoff to be honest about A strict policy costs something. The shared module has to be designed for two callers, which is more work than a handler, and a team under deadline will route around a bar that ignores that. So state it in proportion to the stakes: a public callback surface deserves the full treatment, an internal export link does not need a committee. The principal-level position is not "no endpoints" — it is that **each endpoint should be able to name the caller it exists for**, and that a frontend deployment should not quietly become a second copy of an API somebody else already owns.

  • How would you make a policy like this survive a deadline?
    By making the compliant path the easy one. Most drift is refactor avoidance: a handler is quicker to write than a shared module designed for two callers. Scaffold the module pattern, put the audience question into the template for a new handler so it is asked when it is cheapest, and generate the inventory rather than maintaining it by hand.
  • What single metric tells you this boundary is healthy?
    The number of places where the application makes an HTTP request to itself. It is countable from the codebase, it maps directly onto wasted latency and duplicated validation, and it should trend toward zero. A rising count means rules are migrating out of shared modules and into transport, which is the shape of the drift.
  • When should the behaviour live in another service instead of the frontend deployment?
    When the data and its rules belong to that service and the copy here will drift from them, or when the handler needs a runtime, duration or scaling shape the pages do not have. It stays here when it is genuinely presentation-specific - an aggregation or shape no other consumer wants - and is small and well bounded.
  • How do you retire an endpoint that may still have callers?
    Measure first: traffic by caller over a full business cycle, since some integrations run monthly. Then announce it, add a deprecation signal to the response, watch the residual traffic, and remove on a date rather than on intuition. If no caller appears after a full cycle of logging, removal is a low-risk change worth scheduling rather than deferring.

Fire exits are useful and each one is cheap to cut into a wall, but every extra door is another entrance somebody has to lock, inspect and remember forever.

saying these in an interview costs you the question

  • Treats every client-side data need as an automatic endpoint.
  • Lets business rules live in handlers rather than shared modules.
  • Has the server render fetch from its own deployment over HTTP.
  • Adds endpoints freely but never schedules a removal.
  • Cannot name who, outside the deployment, calls a given endpoint.