Project tour
Read this once before you change code: it tells you where a change belongs (core or your product, API or client, which layer) and which commands you will run every day.
Layout
apps/ keelmint/ api/ NestJS app: composition only (config, database, one import per feature lib) api-e2e/ end-to-end API tests (real Postgres, Mailpit, fake Google and Stripe) client/ Angular app: a thin shell and router; features are lazy-loaded libs site/ this documentation site (Astro + Starlight)libs/ core/ shared by every product - never product-specific shared/ contracts used by API and client: types, ApiError, error codes, product config db/ core Prisma models (prisma/core/*.prisma), base fields, soft delete and tenant extensions api/ one NestJS library per feature: auth, users, roles, organizations, invitations, billing, audit, jobs, mail, notifications, helpdesk, two-factor, profile, storage, security (guards and decorators), env, logging, errors, listing, health, core client/ Angular libraries: *-data-access (API services, stores), *-feature (pages), design-system, i18n, spartan-ui (UI components), common (table, form field), core (layout) keelmint/ the example product's own libraries shared/ product.config.ts (the product's settings) and shared types db/ the product's Prisma models, generated schema, migrations, seed api/todos/ the todo API feature client/ the todo pages and the product's translationstools/ core-plugin/ generators: a new product, an extra client db/init/ SQL that creates each product's databases in the Compose Postgresdeploy/keelmint/ production Docker Compose, Caddyfile, web image, env exampledocs/ these pages (docs/<section>/*.md)Features live in libraries, not in the apps. An API feature library exports only its NestJS module; the app imports that module. A client feature library is a set of lazy-loaded routes. A feature can be tested and moved on its own.
Core and products
libs/core/* is the foundation every product shares. A product (apps/<product>/* and libs/<product>/*) builds on it: its own models, API features, pages, translations and settings in product.config.ts. Core never imports a product, which is what lets you take a newer keelmint release into your fork without conflicts in your own code (see Updating).
Import rules
Every project has tags, and lint (@nx/enforce-module-boundaries) refuses an import that breaks these rules:
| Source tag | May depend on |
|---|---|
| scope:api | scope:shared, scope:api |
| scope:client | scope:shared, scope:client |
| scope:shared | scope:shared |
| scope:core | scope:core |
| scope:product:<name> | scope:core, scope:product:<name> |
| type:feature | type:data-access, type:ui, type:util |
| type:ui | type:ui, type:util |
| type:data-access | type:data-access, type:util (client: type:util only) |
| type:util | type:util |
What this means in practice:
- API code never imports client code and the other way round; both may import
@core/shared. - A feature never imports another feature: move the shared part down into a
data-accessorutillibrary. @core/dbis taggedscope:api, so the Postgres driver can never end up in the browser bundle.- A product’s libraries may use core; core may not use a product.
Everyday commands
Run tasks through Nx with the workspace’s pnpm:
| What | Command |
|---|---|
| API and client together | pnpm dev |
| API only (hot reload, :3000) | pnpm nx serve keelmint-api |
| Client only (:4200) | pnpm nx serve keelmint-client |
| Tests of one project | pnpm nx test <project> |
| Lint of one project | pnpm nx lint <project> |
| API end-to-end tests | pnpm nx e2e keelmint-api-e2e |
| Only what your change affects | pnpm nx affected -t lint,test,build |
| Everything | pnpm nx run-many -t lint,test,build |
| The project graph | pnpm nx graph |
| This docs site (:4321) | pnpm nx serve site |
pnpm nx show projects lists every project; pnpm nx show project <name> shows its targets.