FantasyWiki Backend Architecture
Overview
The backend is a Cloudflare Worker built with Hono. It follows a layered structure:
- Routes: HTTP handling (
backend/src/routes) - Services: business workflows (
backend/src/services) - Repositories: persistence access (
backend/src/repositories), with one implementation per store underrepositories/d1/andrepositories/mongo/
Shared domain models are in the top-level model/ package and are reused across frontend and backend.
Current Repository Layout
FantasyWiki/
├── model/
├── backend/
│ ├── migrations/
│ └── src/
│ ├── index.ts
│ ├── routes/
│ │ ├── auth.ts
│ │ ├── leagues.ts
│ │ └── session.ts
│ ├── services/
│ │ ├── login.ts
│ │ ├── player.ts
│ │ ├── leagues.ts
│ │ └── wikimediaClient.ts
│ ├── composition.ts
│ └── repositories/
│ ├── playerRepository.ts
│ ├── result.ts
│ ├── d1/
│ │ └── playerRepositoryD1.ts
│ └── mongo/
│ └── playerRepositoryMongo.ts
└── frontend/Layer Responsibilities
Routes (backend/src/routes)
- Parse request input
- Enforce auth/HTTP constraints
- Call services
- Map results to HTTP responses
Services (backend/src/services)
- Implement business logic and orchestration
- Depend on repository interfaces (
PlayerRepository) rather than route concerns - Depend on other services whose functionality they need
- Return typed
Resultvalues consumed by routes
Composing services
A service may, and is encouraged to, call another service when that service already provides something useful. Reuse beats restating: a rule implemented twice is a rule that will eventually be two different rules. Take the dependency through the constructor like any repository, so tests can substitute it, and let the caller's Result carry the callee's failure outward rather than re-wording it.
The rule is one of preference, and it holds even when all you want is the data: when what service A needs is offered both by service B and by repository C, reach for B. Go to C only when there is no such B. A service's read is rarely only a read, it dresses rows into DTOs, fills in derived fields, applies the rules that decide what counts as absent, and calling it means you inherit those, including the ones added after you wrote the call. Reaching past it to the repository buys one fewer hop and gives up all of that.
One limit keeps this from becoming a tangle: keep the dependencies acyclic. If A calls B, B must not call back into A. A cycle usually means the shared part wants to be its own service (or to move down into a repository) rather than to be reached for in both directions.
Repositories (backend/src/repositories)
- Define repository contracts (
playerRepository.ts) - Implement each store's access under
repositories/d1/andrepositories/mongo/ - Encapsulate SQL, queries and persistence error handling: a store's own error wording never leaves this layer
A deployment runs on exactly one store, and composition.ts is the only module that picks it. See Persistence Targets.
Runtime and Data
- Runtime: Cloudflare Workers + Hono (
backend/src/index.ts) - Primary persistence: Cloudflare D1 via
dbbinding - Schema and migrations:
backend/migrations/
Testing
- Backend tests run with Vitest, in the Workers pool, against a real database
- Commands:
cd backend && npm run test(D1),npm run testmongo(MongoDB) - Which layer a test may name: and why only
composition.tschooses an implementation, is in Backend Testing
Related
- DDD Building Blocks: which archetype each concept became
- Persistence Targets
- Backend Testing
- Backend Error Constants
- API Naming Rules
- OpenAPI Spec
- Wikimedia Client Architecture
- Sessions and Sign-in Doors
- Contract Settlement: the one caller that is a Cron, not a request
