Skip to content

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 translations
tools/
core-plugin/ generators: a new product, an extra client
db/init/ SQL that creates each product's databases in the Compose Postgres
deploy/keelmint/ production Docker Compose, Caddyfile, web image, env example
docs/ 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 tagMay depend on
scope:apiscope:shared, scope:api
scope:clientscope:shared, scope:client
scope:sharedscope:shared
scope:corescope:core
scope:product:<name>scope:core, scope:product:<name>
type:featuretype:data-access, type:ui, type:util
type:uitype:ui, type:util
type:data-accesstype:data-access, type:util (client: type:util only)
type:utiltype: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-access or util library.
  • @core/db is tagged scope: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:

WhatCommand
API and client togetherpnpm dev
API only (hot reload, :3000)pnpm nx serve keelmint-api
Client only (:4200)pnpm nx serve keelmint-client
Tests of one projectpnpm nx test <project>
Lint of one projectpnpm nx lint <project>
API end-to-end testspnpm nx e2e keelmint-api-e2e
Only what your change affectspnpm nx affected -t lint,test,build
Everythingpnpm nx run-many -t lint,test,build
The project graphpnpm 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.