Packages¶
What this answers: how is the code organised, and what are the dependency rules between packages?
Layout¶
src/
├── app/ # Next.js routes (pages + API)
├── components/ # UI components, organised by purpose
│ ├── ui/ # foundation (Button, Card, Badge, Container)
│ ├── domain/ # domain-specific (PriceTag, StockBadge, SocketChip, RetailerLogo)
│ ├── layout/ # TopNav, Footer
│ ├── landing/ # Hero, HeroBuildCard, PriceTicker, etc.
│ ├── build/ # BuildHeader, BuildSlotRow, CompatPanel, QuickStart
│ └── browse/ # CategoryNav, ProductRow
├── lib/ # build.ts, matching.ts, cn.ts, db.ts
├── rules/ # compat rule pure functions (psuSufficient, gpuFitsCase, ...)
├── scrapers/
│ ├── core/ # http.ts, parse.ts, normalize.ts (shared infra)
│ └── sites/ # one file per retailer
└── generated/prisma/ # Prisma client output (gitignored)
prisma/ # schema.prisma + migrations
scripts/ # one-shots (run-scrapers.ts, build-docs.sh)
tests/ # vitest specs, mirrored against src/
Dependency diagram¶
graph TD
App[src/app]
Components[src/components]
Lib[src/lib]
Rules[src/rules]
Scrapers[src/scrapers]
Prisma[prisma/]
Tests[tests/]
App --> Components
App --> Lib
Components --> Lib
Lib --> Prisma
Lib --> Rules
Scrapers --> Lib
Scrapers --> Prisma
Tests --> Lib
Tests --> Rules
Tests --> Scrapers
Dependency rules¶
- Routes import from components and lib. Not the other way.
- Components import from lib (for utilities) but not from app. A component never knows what route it's rendered in.
- Lib is the lowest application layer. It depends only on Prisma generated types, npm packages, and
src/rules/. - Scrapers and app are siblings. Both consumers of
src/lib. Scrapers never import fromsrc/app(and vice versa). - Rules are pure functions, no I/O.
src/rules/*.tsfiles take inputs, return verdicts. They never read the DB or call APIs. Easy to test, easy to reason about. - Prisma is generated, not hand-written.
npx prisma generaterebuildssrc/generated/prisma. Don't edit that directory.
Code smells we watch for¶
- A component importing Prisma directly — should go via
lib/instead. Components are presentational; data access islib's job. - A scraper importing from
src/app— shouldn't happen. If a scraper needs route logic, that logic belongs inlib/. - A rule that calls the database — defeats the point. Move the data fetch to
lib/, call the rule with the result. - Circular imports between
lib/*modules — usually a sign that two modules share concerns and should merge or split.
Conventions¶
*.tsis server-side or universal. Client-only code is in*.tsx(with'use client') or explicitly marked.- Tests mirror source.
src/lib/matching.tshas its tests attests/lib/matching.test.ts. Hard to lose track of what's tested. - No barrel exports. No
src/lib/index.tsre-exporting everything. Imports are explicit; refactors are clearer.
This layout is small enough that monorepo tooling (turborepo, nx) is unnecessary. If it grows beyond ~20 modules per dir we'll revisit.