Skip to content

Commit ea980b9

Browse files
amal66claude
andcommitted
refactor: decompose route monoliths into per-domain modules with service layers
WHY THIS MATTERS The backend's route files were monoliths — routes/tabular.ts (1,649 lines), routes/documents.ts (1,504), routes/projects.ts (1,139), routes/user.ts (1,132) — each interleaving HTTP parsing, auth checks, storage IO, and business logic inline in a dozen unrelated handlers. In a monolith, every change lands in a giant file where the blast radius is unclear, business logic can only be exercised through a live HTTP stack, and a new contributor cannot tell which lines are "the endpoint" and which are "the feature". For an open-source repo this is the difference between a drive-by contributor shipping a fix and giving up: small files with one concern each are reviewable; 1,600-line route files are not. WHAT IS A SERVICE LAYER A service layer separates WHAT the application does from HOW it is reached. The route (HTTP layer) owns request parsing, validation, and mapping results onto status codes; the service owns business logic and data access, takes its database handle as an explicit parameter, returns typed results (discriminated unions like { ok: false, kind: "not_found" } instead of writing to `res`), and never touches the HTTP request or response. That inversion is what makes logic unit-testable (call the function with a fake db — no server needed), reusable (the async extraction worker calls the exact same functions the SSE route calls), and safe to change (the compiler knows every result shape a route must handle). HOW IT WORKS - src/routes/*.ts (11 files, 7,853 lines) is replaced by src/modules/<domain>/ — chat, project-chat, projects, documents, tabular, user, workflows, library, downloads, case-law, models — each a thin <name>.routes.ts plus <name>.service.ts. Large domains split the service into topic files behind a named-re-export facade (documents: access/upload/versions/download/edits; projects: crud/folders/documents/chats; user: profile/mfa/apiKeys/mcp/account/ export; tabular: reviews/rows/extract/extractRow/generate/ generateStream/chats) so intra-module helpers cannot leak. - lib/tabular/* (from the durable-queues change this builds on) moves into modules/tabular/ — the domain's extraction core, row loaders and route layer now live together; src/lib/ keeps only cross-domain infrastructure (storage, llm, chat, queue, access...). - Streaming endpoints keep their SSE loops in the routes file; only their non-streaming prepare/persist logic moved into services — streaming lifetime and client-abort handling are HTTP concerns. - Pure motion, verified three ways: the endpoint inventory (method+path multiset, 67 endpoints) is byte-identical before and after; tsc is clean; the full suite — 510 tests, including the 11 route-level integration suites that exercise the real express app — passes unchanged. Handler bodies moved verbatim; the only rewrites are the mechanical seam (res.status(...) inside moved code became typed returns mapped back to the identical status/JSON in the route). - DRY within domains only: helpers duplicated across handlers in the same domain (shared_with normalization in projects, the doc-access guard sequence in documents, findSystemWorkflow) now have one copy in their service; similar-but-not-identical code was left alone rather than force-merged. - Zero new dependencies. No logging framework, no validation framework, no observability hooks — organization only, so the diff is reviewable as motion and each future concern can be its own decision. Re-derived against this branch's code from the fork's service-layer refactor (#42, running in the amal66 fork), whose module boundaries and routes/service contract this follows; the fork's pino/OTel/zod adoption was deliberately NOT ported to keep this dependency-free pure motion. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent f59aee9 commit ea980b9

59 files changed

Lines changed: 11709 additions & 8515 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

backend/src/app.ts

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -4,19 +4,19 @@ import express from "express";
44
import cors from "cors";
55
import helmet from "helmet";
66
import rateLimit from "express-rate-limit";
7-
import { chatRouter } from "./routes/chat";
7+
import { chatRouter } from "./modules/chat/chat.routes";
88
import { wordChatRouter } from "./routes/wordChat";
9-
import { projectsRouter } from "./routes/projects";
10-
import { projectChatRouter } from "./routes/projectChat";
11-
import { documentsRouter } from "./routes/documents";
12-
import { libraryRouter } from "./routes/library";
13-
import { tabularRouter } from "./routes/tabular";
14-
import { workflowsRouter } from "./routes/workflows";
9+
import { projectsRouter } from "./modules/projects/projects.routes";
10+
import { projectChatRouter } from "./modules/project-chat/projectChat.routes";
11+
import { documentsRouter } from "./modules/documents/documents.routes";
12+
import { libraryRouter } from "./modules/library/library.routes";
13+
import { tabularRouter } from "./modules/tabular/tabular.routes";
14+
import { workflowsRouter } from "./modules/workflows/workflows.routes";
1515
import { quickActionsRouter } from "./routes/quickActions";
1616
import { workflowAddonsRouter } from "./routes/workflowAddons";
17-
import { userRouter } from "./routes/user";
18-
import { modelsRouter } from "./routes/models";
19-
import { downloadsRouter } from "./routes/downloads";
17+
import { userRouter } from "./modules/user/user.routes";
18+
import { modelsRouter } from "./modules/models/models.routes";
19+
import { downloadsRouter } from "./modules/downloads/downloads.routes";
2020
import { sourceDocumentsRouter } from "./routes/sourceDocuments";
2121
import { auditRouter } from "./routes/audit";
2222
import { manifestPublicKey } from "./lib/manifestSigning";

backend/src/lib/maintenance/staleWork.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@
3232
import { createServerSupabase } from "../supabase";
3333
import { getConversionQueue, conversionJobId } from "../queue/conversionQueue";
3434
import { getExtractionQueue, extractionJobId } from "../queue/extractionQueue";
35-
import { finalizeCell } from "../tabular/tabular.extractRow";
36-
import { finishGenerationIfIdle } from "../tabular/tabular.shared";
35+
import { finalizeCell } from "../../modules/tabular/tabular.extractRow";
36+
import { finishGenerationIfIdle } from "../../modules/tabular/tabular.shared";
3737
import { redisEnabled } from "../dbq/driver";
3838
import { liveDbJobExists } from "../dbq/enqueue";
3939

0 commit comments

Comments
 (0)