Commit ea980b9
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
File tree
- backend/src
- lib/maintenance
- modules
- chat
- documents
- downloads
- library
- models
- __tests__
- project-chat
- projects
- tabular
- __tests__
- user
- __tests__
- workflows
- routes
- workers
- __tests__
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
4 | 4 | | |
5 | 5 | | |
6 | 6 | | |
7 | | - | |
| 7 | + | |
8 | 8 | | |
9 | | - | |
10 | | - | |
11 | | - | |
12 | | - | |
13 | | - | |
14 | | - | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
15 | 15 | | |
16 | 16 | | |
17 | | - | |
18 | | - | |
19 | | - | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
20 | 20 | | |
21 | 21 | | |
22 | 22 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
32 | 32 | | |
33 | 33 | | |
34 | 34 | | |
35 | | - | |
36 | | - | |
| 35 | + | |
| 36 | + | |
37 | 37 | | |
38 | 38 | | |
39 | 39 | | |
| |||
0 commit comments