Split the 5,441-line index.js into an 86-line bootstrap, thin domain
route modules, shared lib/ helpers, and a repo/ layer that isolates the
DEV_MOCK branches (repo/*.mock.js). Repo methods return {statusCode, body};
routes are one-line wrappers. Pin search_path to "transit.bus" and
document the /bus/ prefix with legacy root aliases (Deprecation header).
Parity verified: 57/57 byte-identical on the stub differential harness
plus 24/24 live read-only responses vs the original against real
PostgreSQL. The only intentional behavior change: mock reorder/delete
bus-line endpoints no longer require a live DB (previously 500 in
DEV_MOCK=1 without PG). DEV_MOCK gate and node --check clean.
17 KiB
Phase 3 — transit_api Module Split — Agent Instructions
Goal: Break the 5,441-line transit_api/index.js into a bootstrap file + domain
route modules + shared libs, and eliminate the double-implementation (DB vs
DEV_MOCK) via a repository layer. No behavior change. No endpoint changes.
Prerequisite: the /bus/ prefix refactor is already applied
(const busRouter = express.Router() at index.js:99, double-mount /bus + /
with Deprecation at the end of the file). Do not re-create it.
1. Rules
- Pure code moves only. Every endpoint path, JSON shape, error code, and
DEV_MOCKbehavior must stay byte-for-byte identical to today. node --check <file>must pass for every new/edited file.- The
appremains the owner of the global middleware and the two mounts:app.use((req,res,next)=>…)— theres.jsonerror-reporting wrapper (index.js:54–71)- The double-mount
app.use('/bus', busRouter)+app.use('/', deprecMw, busRouter) app.use('/health', healthRouter)(unprefixed, per /bus/ refactor rule 1)
- All SQL keeps unqualified table names (resolution via
search_path="transit.bus"). - One module per endpoint cluster (map below). Each route module ends with
module.exports = router;(anexpress.Router()). - After finishing each stage you must boot mock mode and curl the touched endpoints (section 8).
2. Target layout
transit_api/
├── index.js → bootstrap (~120 lines)
├── db.js (unchanged)
├── middleware.js → add adminMiddleware (moved from index.js:282)
├── lib/
│ ├── env.js → DEV_MOCK, ERROR_LOG_API_URL
│ ├── log.js → log()
│ ├── errors.js → friendlyErrorMessage()
│ ├── report-error.js → reportError()
│ ├── validate.js → isValidDateKey()
│ ├── schedule.js → the ~20 pure schedule/conflict helpers (section 4)
│ └── schema.js → ensureScheduleStopProgressTable()
├── mock/
│ └── store.js → singleton _mock object + seed block (index.js:85–280)
├── repo/
│ └── index.js → DEV_MOCK ? mockRepo : pgRepo (Stage D)
└── routes/
├── health.js ├── stops.js ├── lines.js
├── fleet.js ├── drivers.js ├── daily.js
├── routes.js ├── navigation.js ├── gps.js
├── schedules.js ├── weekly.js └── adminSchedules.js
3. Route → module map (exact)
| Module | Endpoints |
|---|---|
health.js |
GET /health |
daily.js |
GET /schedule/daily, GET /driver/schedules/daily |
routes.js |
GET /routes/:routeId, PATCH /routes/:routeId/status |
stops.js |
GET/POST /admin/stops, PATCH/DELETE /admin/stops/:stopId |
lines.js |
GET /bus-lines/:busLineId/stops, GET/POST /admin/bus-lines, PATCH/DELETE /admin/bus-lines/:busLineId, GET/POST /admin/bus-lines/:busLineId/stops, POST /admin/bus-lines/:busLineId/stops/reorder, DELETE /admin/bus-lines/:busLineId/stops/:routeStopId |
fleet.js |
GET/POST /admin/fleet/buses, PATCH/DELETE /admin/fleet/buses/:busId |
drivers.js |
GET /admin/drivers, GET /admin/shift-drivers |
navigation.js |
POST /navigation/route |
gps.js |
POST /gps/track, GET /gps/route/:routeId |
schedules.js |
GET /schedules, /schedules/overview, /schedules/recurring, /schedules/:scheduleId, /schedules/:scheduleId/stops, PATCH /driver/schedules/:scheduleId/status, POST /driver/schedules/:scheduleId/stops/:stopOrder/passed, GET /public/schedules/:scheduleId/progress |
weekly.js |
PATCH /driver/weekly-schedules/:scheduleId/status, GET/POST/PUT/DELETE /admin/weekly-schedules* incl. /bulk and /:scheduleId/shift |
adminSchedules.js |
GET/POST/PUT/DELETE /admin/schedules* incl. /bulk, POST /admin/check-driver-conflicts, POST /admin/assign-schedules |
4. Stage A — extract lib/ helpers
Create the files listed in section 2 by moving code verbatim from index.js:
env.js:DEV_MOCK = process.env.DEV_MOCK === "1",ERROR_LOG_API_URL = process.env.ERROR_LOG_API_URL || "https://error.xtransit.testinstance.nl/error_log".log.js: thelog(level, message, meta)JSON logger (index.js:8–11).errors.js:friendlyErrorMessage(statusCode)(index.js:29–35).report-error.js:reportError({partofservice, error})(index.js:37–52); requiresenv.validate.js:isValidDateKey(value)(index.js:21–27).schedule.js: move these pure functions verbatim —parseWeekdaysValue,toNumberOrNull,parseCommaSeparatedValues,toDateKey,expandRecurringScheduleRows,mapAdminScheduleRow,scheduleMatchesFilters,parseScheduleFilters,formatWeekdayLabel,buildDepartureTimes,calculateDepartureTimes,isRecurringSchedule,scheduleOccursOnDate,materializeScheduleOverviewRow,mapRecurringOccurrenceRows,materializeScheduleRowsInRange,doSchedulesDateOverlap,checkDriverConflict,checkBusConflict.schedule.jsmayrequire('./schedule')internally; nodb,env, or HTTP dependencies.schema.js:ensureScheduleStopProgressTable()(index.js:874–966); skips whenDEV_MOCK(readsenv), usesdb.
Verify: node --check all files; node -e "require('./lib/schedule')" to confirm
inline requires resolve.
5. Stage B — isolate mock state
Create mock/store.js from the _mock object and seed block (index.js:85–280,
everything between const DEV_MOCK and the //=== HELPER FUNCTIONS header):
- Move
_mock = { stops, routeStops, busLines, fleetBuses, schedules, scheduleStopProgress, drivers, nextStopId, … }and the mock driver seed + schedule seed verbatim into the module, freezing only the shape — keep it a mutable singleton. - Export
module.exports = { store }(do NOT mutate any field names casing), plus anything the seed block references. - index.js should no longer define
_mock; its handlers later read it via the store. Until Stage C moves the handlers, index.jsrequires the store and aliases_mocktostoreso the existing handler code compiles unchanged:const { store: _mock } = require('./mock/store');
Verify: boot mock mode; a stop/line/schedule read+write round-trips still work.
6. Stage C — split route modules
For each module in section 3, in dependency-light→heavy order
(health → stops → lines → fleet → drivers → navigation → gps →
routes → daily → weekly → adminSchedules → schedules):
- Create
routes/<name>.js:const express = require("express"); const router = express.Router(); const { authMiddleware, adminMiddleware } = require("../middleware"); // + its lib/mock/repo requires // <moved handlers, unchanged — register on `router`> module.exports = router; - Move the handler block verbatim from index.js (the
app.method calls becomerouter.calls). Do not rewrite handler logic yet —DEV_MOCKbranches stay inside the handlers for now; they consume_mockvia themock/storesingleton and DB viadb. - Move
adminMiddlewareintomiddleware.js(verbatim from index.js:282–288), add it tomodule.exports. TheDEV_MOCK/db/helper requires each module needs are added to its header. - Remove the moved block from index.js.
Keep index.js: global middlewares, health mounting, busRouter
declaration, the final double-mount, the startup ensureScheduleStopProgressTable
call, app.listen, and module.exports = app. At the end of Stage C index.js
must read:
const busRouter = express.Router();
busRouter.use(require("./routes/stops"));
busRouter.use(require("./routes/lines"));
/* ... every module except health ... */
app.use("/health", require("./routes/health"));
app.use("/bus", busRouter);
app.use("/", (req, res, next) => {
res.set("Deprecation", 'true; date="2030-01-01"');
next();
}, busRouter);
Verify: after EACH module — node --check, mock boot, curl at least a GET and a
mutating endpoint of that module under /bus/..., plus one legacy root alias.
7. Stage D — repository layer (kills the 40 DEV_MOCK branches)
Create repo/index.js:
const { DEV_MOCK } = require("../lib/env");
const pg = { ...require("./stops"), ...require("./lines"), /* … */ };
const mock = { ...require("./stops.mock"), /* only modules with mock branches */ };
module.exports = DEV_MOCK ? { ...pg, ...mock } : pg;
Implemented design (deviation from the original plan, documented): each
domain lives in repo/<domain>.js (PostgreSQL branch) and, where the original
handler had an if (DEV_MOCK) branch, a repo/<domain>.mock.js (mock branch).
repo/index.js splices the mock methods over the pg ones when DEV_MOCK=1.
Endpoints with no mock branch are pg-only and are therefore used in both modes
(routes, daily, weekly, gps, navigation, health, listShiftDrivers).
Instead of returning native rows, every repo method takes the Express req
and returns { statusCode, body } — the exact payload the original handler
sent via res.status(n).json(body). Route modules are one-line wrappers:
router.get("/schedules", async (req, res) => {
const out = await repo.listSchedules(req);
return res.status(out.statusCode).json(out.body);
});
This is required for byte-parity: the mock and DB branches of a single original
handler build different JSON shapes (the "unify via
row.busLineId ?? row.bus_line_id" idea in the first plan would have changed
responses). Repo branches are verbatim copies of the moved handler bodies.
Implemented methods (per repo files):
repo/stops.js+.mock.js:listStops,createStop,updateStop,deleteStoprepo/lines.js+.mock.js:listLineStops,listLines,createLine,updateLine,listLineStopsDetailed,addLineStop,reorderLineStops,deleteLine,deleteLineStoprepo/fleet.js+.mock.js:listFleetBuses,createFleetBus,updateFleetBus,deleteFleetBusrepo/drivers.js+.mock.js:listAdminDrivers,listShiftDrivers(shift-drivers is pg-only, shared)repo/route.js(pg-only):getRoute,updateRouteStatusrepo/daily.js(pg-only, usesDEV_MOCKguard for its fallbacks):listDailySchedules,listDriverDailySchedulesrepo/weekly.js(pg-only):updateWeeklyScheduleStatus,listWeeklySchedules,createWeeklySchedule,bulkCreateWeeklySchedules,updateWeeklySchedule,updateWeeklyScheduleShift,deleteWeeklySchedulerepo/navigation.js(pg-only):saveNavigationRouterepo/gps.js(pg-only):saveGpsTrack,listRouteGpsPointsrepo/adminSchedules.js+.mock.js:listAdminSchedules,createAdminSchedule,updateAdminSchedule,bulkCreateAdminSchedules,deleteAdminSchedule,checkDriverConflicts(shared, has its own tinyDEV_MOCK ? mock.schedules : dbternary),assignSchedulesrepo/schedules.js+.mock.js:updateDriverScheduleStatus,listSchedules,getSchedulesOverview,listRecurringSchedules,getScheduleById,getScheduleStops,markStopPassed,getPublicScheduleProgress
grep -n DEV_MOCK transit_api/index.js transit_api/routes/*.js is clean; DEV_MOCK
appears only in lib/env.js, lib/schema.js, repo/index.js, repo/daily.js
(fallback guard), repo/adminSchedules.js (shared conflict ternary), and
mock/store.js (seed guard).
Verify after each module (section 8).
8. Verification (gate after every stage/module)
cd transit_api
node --check index.js && node --check lib/*.js routes/*.js repo/index.js mock/store.js
ADMIN_USER_CODES=42 DEV_MOCK=1 JWT_SECRET=devsecret npm start # mock boot
Curl checks (against http://localhost:5001):
GET /bus/health→ 404;GET /health→ 200{status:"ok"}.GET /bus/admin/stops→ 401missing_token.- For each touched module:
GET /bus/<endpoint>→ 200 (mock shape), one mutating call → 201/200, and one legacy aliasGET /<endpoint>→ 200 +Deprecationheader.
9. Stage E — final pass / definition of done
grep -n "DEV_MOCK" transit_api/index.js routes/*.js→ onlylib/env.js,repo/index.js, andlib/schema.jsmay reference it.- index.js is ≤ ~150 lines.
npm startboots in mock mode, and (where a DB is reachable) prod mode with the"transit.bus"search_path;GET /healthshowsdatabase:"ok".- Full curl sweep of all 47 endpoint patterns under
/bus/at least once. - Report per-module verification results at the end.
10. Implementation status (Stages B–E)
Completed and verified. Counts at time of writing:
transit_api/index.js 86 lines (bootstrap; target ≤ ~150)
transit_api/routes/*.js 11 modules (12 incl. health) — thin wrappers
transit_api/repo/*.js 17 files (8 pg + 2 with .mock twins etc.)
transit_api/mock/store.js 183 lines
transit_api/lib/*.js Stage A modules (verified byte-identical)
Verification method. Two layers:
- A differential harness (
/tmp/opencode/harness2.js) that stubsdb.jsinrequire.cache(DB_QUERY_BLOCKED/DB_POOL_BLOCKED), boots the original monolith (/tmp/opencode/index.c-original.js, = post-A/pre-B/C snapshot) and the refactoredindex.jsside by side, fires identical request sequences, ISO-date normalizes, and byte-compares JSON. This covers both modes and the DB error path. - Live boots against the real PostgreSQL (once
PG_HOSTin.envwas corrected from192.168.2.66to reachablePG_HOST), comparing the original and refactored apps response-for-response on real data (above).
Results:
- 57/57 compared responses byte-identical (deviation-free) across the full
48-endpoint inventory +
/health+ legacy aliases + no-auth case. - Live DB verification (added after PG access restored): with PostgreSQL
reachable at
PG_HOST(schematransit.bus, verified viaSELECT current_schema() = 'transit.bus'), the original monolith and the refactored app were booted side by side in prod mode and fired with identical read-only requests. 24/24 responses byte-identical (status, body, and the legacyDeprecationheader) against real data (/tmp/opencode/live-parity.js, baseline was copied back in from/tmp/opencode/index.c-original.jsand removed afterwards). This closes the one gap the stub harness had: PG success paths on real rows. Note:GET /bus/schedules500s on the live DB (column s.bus_id does not exist) — this exists in the original too (identical 500, byte-identical), so it is a pre-existing DB/data-model mismatch with this environment, not a refactor regression, and is out of scope here. - Live mock boot:
DEV_MOCK=1 npm startboots (PG now reachable),/healthreportsdatabase:"ok", mock stops/schedules served, legacy aliases setDeprecation: true; date="2030-01-01". - 3 documented, intentional improvements (mock-only; the original required a
live DB even in
DEV_MOCK=1because it calleddb.pool.connect()before the mock branch):POST /admin/bus-lines/:busLineId/stops/reorderDELETE /admin/bus-lines/:busLineIdDELETE /admin/bus-lines/:busLineId/stops/:routeStopIdOld (no DB): 500 empty. New (mock): proper 200/400 responses fromrepo/lines.mock.js. In PG mode both behave identically.
Quirks preserved verbatim (do not "fix"):
POST /driver/schedules/:scheduleId/stops/:stopOrder/passedmock branch reads an undeclareddelayMinutes→ReferenceError→ 500 in both modes (repo/schedules.js+repo/schedules.mock.js).bulkCreateWeeklySchedulesand admin bulk normalizationthrowoutsidetry/ before it; pgdb.pool.connect()calls stay beforetry(hang when PG is unreachable) — control flow preserved exactly.repo/adminSchedules.jslistAdminSchedules/createAdminScheduleuse the literaltable_schema='transit'whilerepo/schedules.jsoverview/recurring use'transit.bus'— matching the original's inconsistent literals.- The
GET /schedules/overviewhandler'selse if (DEV_MOCK && …)enrichment is dead code in the original (the mock branch returns earlier); it is NOT copied intorepo/schedules.mock.js.
End-of-Stage checks:
grep -rn DEV_MOCK index.js routes/→ clean.node --check→ passes forindex.js, allroutes/*.js,repo/*.js,mock/store.js,middleware.js,db.js.git status/git diff→ the only file changes intransit_api/are the Stage splits (no stubs or temp files left behind; the parity baselineindex.c-original.jsis only ever copied in from/tmp/opencodeduring runs and removed afterwards).