# ShipRex CMS/API — Feature Development Guide (the standard way)

> This is the **how**. It defines the one correct way to add a feature so every
> module looks the same. Read [architecture.md](architecture.md) first for the
> mental model, and [database-guide.md](database-guide.md) for data changes.
>
> Every PRP (see [PRP-TEMPLATE.md](PRP-TEMPLATE.md)) turns this guide into an
> ordered task list. **Do not invent new patterns** — copy the reference module
> (**Knowledge Base**) and only ADD what's new.

---

## 0. Definition of a "feature"

A feature is a **module** under `src/modules/<feature>/` that may expose:
- **Public API** endpoints (for the website/integrators) — JSON, API-key + scope.
- **Admin** screens (for the ShipRex team) — EJS, session.
- Its own **model(s)** + **migration** + optional **seeder**.
- An entry in the **Postman collection** (the contract).

Most features have all of the above. Some are admin-only or API-only — drop the
parts you don't need, keep the structure.

---

## 1. The canonical anatomy (copy this)

```
src/modules/<feature>/
├─ <feature>.service.js          # business logic + Sequelize queries; throws ApiError
├─ <feature>.api.controller.js   # public JSON controller (+ exported zod schemas)
└─ <feature>.client.js / .provider.js   # only if it talks to an external service

src/admin/<feature>.controller.js   # admin EJS controller(s)
src/models/<feature>.js             # model(s) — one per table
src/routes/api/v1/<feature>.routes.js   # public routes
src/views/<feature>/*.ejs           # admin screens (list/form/show)
migrations/<timestamp>-<change>.js  # schema
seeders/<timestamp>-<seed>.js       # optional sample/bootstrap data
```

Reference implementation to read end-to-end: the **Knowledge Base** module
(`src/modules/knowledge-base/`, `src/admin/kb.controller.js`,
`src/models/kbArticle.js` + `kbCategory.js`, `src/routes/api/v1/kb.routes.js`,
`src/views/kb/*`, the `init-schema` migration).

---

## 2. Layer responsibilities (don't blur these)

### Service (`<feature>.service.js`)
- The **only** place with business logic and Sequelize calls.
- Pure: takes plain args, returns plain data. **Never** reads `req`/`res`.
- Throws `new ApiError(status, code, message, details?)` on domain errors
  (e.g. `throw new ApiError(404, 'not_found', 'Article not found')`).
- Owns cross-cutting concerns like slug uniqueness, search, pagination shaping.
- Returns list results as `{ rows, count, page, limit, pages }` (see KB service).

### Public controller (`<feature>.api.controller.js`)
- Thin. One exported handler per endpoint, each wrapped in `asyncHandler`.
- **Exports its zod schemas** (e.g. `createSchema`, `listQuery`) so routes can
  `validate(...)` with them.
- Formats output with `apiResponse.ok / created`. Never builds error JSON
  (throw `ApiError` or let the service throw).

### Admin controller (`src/admin/<feature>.controller.js`)
- Thin. Calls the same service, then `res.render('<feature>/<view>', data)`.
- Uses `req.flash('success'|'error', msg)` then `res.redirect(...)` after writes
  (Post/Redirect/Get). Forms that update/delete use the `?_method=PUT|DELETE`
  override (already wired by `webHelpers.methodOverride`).

### Model (`src/models/<feature>.js`)
- Schema only. `snake_case` columns (`underscored: true` is global). Add tiny
  helpers (e.g. `verifyPassword`) only when they belong to the row.
- Associations are declared centrally in `src/models/index.js`, not in the model
  file.

### Routes
- **Public:** `apiKeyAuth('<scope>')` → optional `strictLimiter` for expensive
  endpoints (chat, contact, ticket-create) → `validate(schema[, 'query'])` →
  handler.
- **Admin:** mounted under the `requireAdmin` block in `src/routes/admin/index.js`.

---

## 3. Conventions (memorize)

**Naming**
- Files: `kebab-case` for routes/views (`kb.routes.js`, `categories.ejs`), but the
  existing module files use `<feature>.service.js` / `<feature>.api.controller.js`.
- DB columns: `snake_case`. Models reference them as defined; Sequelize maps
  `camelCase` attribute names only if you set them — we use `snake_case`
  attributes directly (e.g. `is_active`, `created_at`).
- Slugs/refs: use `helpers.slugify`, `helpers.publicId`, `helpers.ticketReference`.

**Response shape (public API)**
- Success: `ok(res, data, meta?)` or `created(res, data)`.
- Lists: pass `meta = { page, limit, total, pages }`.
- Errors: throw `ApiError`. The codes are machine-readable snake_case
  (`not_found`, `validation_error`, `insufficient_scope`, `rate_limited`).

**Validation**
- Always `validate(schema)` at the route. `validate` **replaces** `req.body` /
  `req.query` with the parsed (coerced) value — downstream code reads the clean
  data. Use `z.coerce.number()` for query numbers.

**Errors & logging**
- Domain errors → `ApiError`. Unexpected throws are caught by `asyncHandler` →
  `errorHandler` (logs 5xx with stack, returns safe JSON/HTML).
- Log with `logger`, never `console.*`.

**Config & secrets**
- Read everything from `config` (`src/config/index.js`). Add new env keys there
  *and* to `.env.example`. Never read `process.env` in feature code. Never commit
  secrets.

**External calls (email, AI, etc.)**
- Wrap third-party SDKs/REST in a `*.client.js` or `*.provider.js` with graceful
  degradation (see `brevo.client.js`, `ai.provider.js`). Side-effects like email
  must **never block** the main flow — fire-and-log via `sendAndLog`.

**Admin UI**
- Reuse `layouts/admin.ejs`, `partials/flash.ejs`, `partials/pagination.ejs`, and
  the CSS classes in `public/css/admin.css` (`.card`, `.table-wrap`, `.toolbar`,
  `.btn`, `.badge.<color>`, `.form-row`). Don't add a CSS framework or hand-roll
  new components a partial already provides.

---

## 4. The standard build order (what every PRP follows)

1. **Data** — model(s) in `src/models/`, register + associate in
   `src/models/index.js`, write the **migration** (and seeder if needed). Run it
   on a scratch DB. → see [database-guide.md](database-guide.md).
2. **Service** — `<feature>.service.js`: methods for each operation, list shaping,
   `ApiError` on domain failures.
3. **Public API** — `<feature>.api.controller.js` (+ zod schemas), routes in
   `src/routes/api/v1/<feature>.routes.js`, mount in `src/routes/api/v1/index.js`,
   add the **scope** (to `ApiClient.scopes` defaults + document it).
4. **Admin** — `src/admin/<feature>.controller.js`, routes in
   `src/routes/admin/index.js`, views in `src/views/<feature>/`, **nav item** in
   `src/views/layouts/admin.ejs`.
5. **Contract** — update the **Postman collection** + its README with the new
   endpoints (request, body, example response, scope/auth).
6. **Docs** — note any new env keys in `.env.example`; update this guide only if
   you introduced a genuinely new shared pattern.

Keep each step an independently committable task (see Workflow below).

---

## 5. Integration touchpoints (the files every feature edits)

```yaml
MODELS:        src/models/<feature>.js  (+ register/associate in src/models/index.js)
MIGRATION:     migrations/<timestamp>-<change>.js   (mirror the model exactly)
PUBLIC ROUTES: src/routes/api/v1/<feature>.routes.js  (+ mount in api/v1/index.js)
ADMIN ROUTES:  src/routes/admin/index.js   (under the requireAdmin block)
ADMIN VIEWS:   src/views/<feature>/*.ejs
ADMIN NAV:     src/views/layouts/admin.ejs   (sidebar <a> + active key)
SCOPE:         src/models/apiClient.js (default scopes) + apiKeyAuth('<scope>')
CONFIG/ENV:    src/config/index.js + .env.example   (only if new settings)
CONTRACT:      docs/postman/ShipRex_API.postman_collection.json (+ postman/README.md)
```

---

## 6. Validation loop (this stack has no front-end build — use these)

There is no TypeScript/lint/Vite step here. The standard checks are:

### Level 1 — Static / load
```bash
# App + all modules require cleanly (catches syntax & wiring errors)
node -e "require('dotenv').config(); require('./src/app'); require('./src/models'); console.log('OK: app + models load')"
```

### Level 2 — Schema
```bash
# Against a scratch DB (see database-guide.md for creating shiprex_cms_test)
npx sequelize-cli db:migrate
npx sequelize-cli db:seed:all      # if the feature seeds data
```

### Level 3 — Runtime (boot + curl the contract)
```bash
PORT=3999 node app.js &            # boots; /health shows DB up
curl -s localhost:3999/api/v1/status
curl -s -H "x-api-key: $KEY" localhost:3999/api/v1/<feature>/<endpoint>
# Negative: no key → 401; bad body → 422 with details[]
```

### Level 4 — Admin
- Log in at `/admin`, open the new screen, exercise create/edit/delete; confirm
  flash messages and that the list reflects changes (Post/Redirect/Get).

### Level 5 — Contract
- Run the new requests from the **Postman collection** against a running server;
  confirm the live responses match the documented examples (no invented fields).

> Tip: the smoke-test commands used during initial build are a good template —
> see the "Verified working" section of the root `README.md`.

---

## 7. Security rules (non-negotiable)

- **Two realms stay separate.** No admin logic under `/api`; no API-key access to
  `/admin`. Don't add a "convenience" endpoint that leaks admin data publicly.
- **Every public write is rate-limited** (`strictLimiter`) and **validated**
  (`zod`). Add a **honeypot** field for public forms (see `contactSchema.website`).
- **Every public endpoint declares a scope.** Don't widen an existing client's
  scopes silently.
- **Secrets only in `.env`/cPanel env** — never in code, never in Git, never in
  the Postman collection (use Postman **variables**).
- **Admin mutations are role-aware** where it matters (`requireRole`).
- **Never trust `req` data** past `validate`; never interpolate it into raw SQL
  except via Sequelize `replacements` (see the FULLTEXT query in `kb.service.js`).

---

## 8. Workflow rules

- **Branch** off `master` per feature/PRP. Don't commit straight to `master`.
- **One commit per completed PRP task**, message: `PRP-XXX Task N: <what>`.
- A task that adds a public endpoint **must** update the Postman collection in the
  **same** commit (contract and code never drift).
- A task that changes the schema ships the **migration in the same commit** as the
  model change.
- Run the relevant validation level before committing.
- Commit footer:
  `Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>` (when generated with Claude).

---

## 9. Anti-patterns to avoid

- ❌ Putting Sequelize queries or business logic in a controller.
- ❌ Building error JSON by hand instead of throwing `ApiError`.
- ❌ Reading `process.env` directly in feature code.
- ❌ Skipping `validate` / accepting unvalidated `req.body`.
- ❌ A public endpoint without a scope, rate limit, or Postman entry.
- ❌ A model change without a matching migration (or vice-versa).
- ❌ Blocking a request on a third-party call (email/AI) instead of `sendAndLog`/async.
- ❌ Re-implementing pagination, flash, layout, slugify, or the response envelope.
- ❌ Exposing admin data through `/api`, or accepting API keys on `/admin`.
- ❌ Hardcoding secrets anywhere, including the Postman collection.
