For AI agentsA Markdown index of these docs is at /llms.txt. Every page is also Markdown: add .md to its address.

Agent Rules / Chimeras

Why did renaming an Express route file break my client?

Chimera CH-06 · Express (Node, CommonJS)

Rules it tests: AR-37, AR-38

app.js lists the routes/ directory, requires each file by a path it builds at startup, and mounts the router under a prefix taken from the file name. The file name is now a public URL, and nothing in the source names that URL. Rename invoice.js to invoices.js and the API moves to a new path while the client keeps calling the old one. Write an explicit route table that imports each router by a literal path and mounts it with a literal prefix.

The code below is from a small example project we wrote for this page. The break is shown by file and line, and we ran the project's own tests and server to confirm it.

1. The setup

Express makes this pattern short, and the parts are good ones. express.Router creates a mountable set of route handlers, app.use mounts it at a path, and a directory scan means a new route file needs no edit to the app. The billing API has three route files, each calling into a file in services/.

javascript// before/app.js const routesDir = path.join(__dirname, "routes"); for (const file of fs.readdirSync(routesDir)) { // line 10 const name = path.basename(file, ".js"); // line 11 app.use("/" + name, require("./routes/" + file)); // line 12 }
javascript// before/routes/invoices.js const { getInvoice } = require("../services/invoices"); ... module.exports = router; // line 16
javascript// before/client/api.js const getInvoice = (id) => fetchJson("/invoice/" + id); // line 12

The tests call the services directly (tests/services.test.js) and request one route through a live server (tests/app.test.js, GET /customers/1).

2. Where the call path hides

  • The require argument is built at runtime. Node documents require(id) as taking a module identifier, with a relative path resolved against the file that calls it. At app.js:12 the identifier is "./routes/" + file, and file comes from the directory listing at line 10. No source line contains the text ./routes/invoices.
  • The URL prefix is computed too. name at app.js:11 is the file name without .js. The routing guide mounts a router with a literal path, app.use('/birds', birds). Here the path is "/" + name, so /invoices appears in no file.
  • The router has no importer. routes/invoices.js ends with module.exports = router at line 16. A search for require("./invoices") or for the router's name finds nothing, because the only require that loads it is the loop.
  • The client holds the URL as a string. client/api.js:12 writes "/invoice/". It is joined to the server only by an HTTP request.

3. The edit that breaks it, and which call is left wrong

The route files for customers and products are plural, so routes/invoice.js is renamed routes/invoices.js. The edit is finished in the files it touches: the service import inside the route file still works, and the service tests call getInvoice directly and pass.

javascript// before/routes/invoices.js (edit applied) // This file was routes/invoice.js and is now routes/invoices.js

Nothing at startup complains. The loop at app.js:10 finds invoices.js, name becomes invoices, and line 12 mounts the router at /invoices. The prefix /invoice no longer exists.

The call left wrong is client/api.js:12, fetchJson("/invoice/" + id). That request reaches no route. The Express FAQ describes the result: a 404 is not an error, it means Express ran all middleware functions and routes and found that none responded. fetchJson throws at api.js:6 with the status in the message. Customers (api.js:11) and products (api.js:13) are unchanged and still work, so a smoke test on either passes.

No test reaches that URL. tests/app.test.js:25 is written as the test that would catch it, marked todo with the reason. With Express 5.3.0 on Node 24, node --test on before/ reports 4 passed and 1 todo and exits 0: the todo test gets 404 where it expects 200, and the run still counts as green. On after/ all 5 tests pass.

A second edit has the same shape. A scratch copy such as routes/invoices.old.js is also listed at line 10, so name is invoices.old and the router is mounted at /invoices.old the next time the process starts. The directory contents are the program, and a file added by an editor or a merge changes the URL map without a line of code changing. The mount order is the order readdirSync returns the names; the Node page for fs is the place to check whether it states one, and this page does not claim it.

4. The fix

One file lists every router by a literal path and mounts it with a literal prefix. Each router file is unchanged. app.js mounts the table once.

diff--- before/app.js+++ after/app.js-const fs = require("node:fs");-const path = require("node:path"); const express = require("express"); +const routes = require("./routes");+ const app = express(); app.use(express.json());--// Every file in routes/ becomes a URL prefix named after the file.-const routesDir = path.join(__dirname, "routes");-for (const file of fs.readdirSync(routesDir)) {- const name = path.basename(file, ".js");- app.use("/" + name, require("./routes/" + file));-}+app.use(routes);
diff--- /dev/null+++ after/routes/index.js+const express = require("express");++const customersRouter = require("./customers");+const invoicesRouter = require("./invoices");+const productsRouter = require("./products");++const routes = express.Router();++routes.use("/customers", customersRouter);+routes.use("/invoices", invoicesRouter);+routes.use("/products", productsRouter);++module.exports = routes;
diff--- before/client/api.js+++ after/client/api.js-const getInvoice = (id) => fetchJson("/invoice/" + id);+const getInvoice = (id) => fetchJson("/invoices/" + id);

5. Why the fix works

  • Every router has an importer. require("./invoices") in routes/index.js is a string literal that a text search and a rename tool both find. Moving routes/invoices.js is followed by a tool that updates imports, and a missed one fails at startup: Node documents that require() throws a MODULE_NOT_FOUND error when the path does not exist.
  • The URL is a literal beside its router. routes.use("/invoices", invoicesRouter) is the one place the public path is written. Renaming a file no longer renames a URL, and changing a URL is an edit to a line anyone can search for.
  • The prefix and the client can be checked against each other. A search for /invoices lands on the mount line, the client and the test. The test at after/tests/app.test.js requests /invoices/501 and asserts the total.
  • A stray file does nothing. A scratch copy in routes/ is not required by anything, so it adds no URL.
  • The cost is one line per router. Adding a route file means adding two lines to routes/index.js. That edit is the record that a new URL is public, and the Express middleware guide's description of an app as a series of middleware calls means the line order in this file is also the order the routers are tried.

6. Rules that apply

  • AR-37: do not load files with a computed path where an agent edits them. require("./routes/" + file) is the CommonJS form of the dynamic import in that rule.
  • AR-38: register routes and middleware explicitly in one router module, with a literal import and a literal prefix per router.

7. References

Express (fetched 2026-10-30):

  • Routing guide (express.Router, mounting with app.use('/birds', birds), mergeParams): https://expressjs.com/en/guide/routing.html
  • Using middleware (optional mount path, an app as a series of middleware calls): https://expressjs.com/en/guide/using-middleware.html
  • FAQ, handling 404 responses (a 404 means no middleware or route responded): https://expressjs.com/en/starter/faq.html

Node.js (fetched 2026-10-30):

  • Modules, require(id) (relative paths resolve against the calling file, MODULE_NOT_FOUND when the path does not exist, cached after first load): https://nodejs.org/api/modules.html
  • File system, fs.readdirSync: https://nodejs.org/api/fs.html

Last verified

For agentsthis page as Markdown/agent-readable/ch-06.mdllms.txt