This file provides guidance to Claude Code (claude.ai/code) or any other agentic AI's such as Gemini/Codex/Copilot/etc when working with code in this repository.
- do not add any try catch in routes. Any errors thrown should be handled by global error handler middleware.
- whenever a new migration file is created, run
npm run migration:runto run the migration from the root.
# the following are dummy example values
HOSTINGER_VPS_HOST=145.223.71.24
HOSTINGER_VPS_USER=deploy
HOSTINGER_VPS_APP_DIR=/var/www/pocket_pixel
HOSTINGER_VPS_SSH_KEY=<privatesshkey>
API_ENV=<the entire content of .env>
All commands run from the repo root (npm workspaces).
# Dev
npm run dev:api # API with ts-node-dev (respawn), port 4000
npm run dev:ui # Next.js dev server
# Build
npm run build:shared # rebuild shared package — REQUIRED after editing packages/shared/src
npm run build # build all workspaces
npm run build:prod # build shared -> ui -> api in dependency order
# Run (production-style)
npm run start # node dist/index.js (also serves the UI static export)
# Migrations (TypeORM)
npm run migration:generate # generate from entity changes
npm run migration:run # apply pending migrations
npm run migration:revert
# Tests
npm run test:api # Jest (API unit tests)
npm run test:api -- auth.service # run a single API test by name/path
npm run test:api -- --runInBand # how CI runs them
npm run test:ui:e2e # Playwright e2e
npm run test:ui:e2e-ui # Playwright in UI modeThere is no lint script; formatting is enforced with Prettier (npx prettier --write <file> — singleQuote, trailingComma: all, printWidth: 200).
npm-workspaces monorepo with three packages:
packages/api— Express + TypeORM + SQLite (better-sqlite3). REST API, also serves the UI static export in production.packages/ui— Next.js 14 (App Router), React 18, Tailwind. Talks to the API viaaxiosclients insrc/lib/api.packages/shared(@expense-tracker/shared) — request/response DTOs insrc/contracts/, imported by both API and UI. Never redefine these DTOs locally; rebuild withnpm run build:sharedafter changes.
Layered: route → service → repository → entity.
- Routes are thin. Each resource has an aggregator
routes/<feature>.routes.tsthat mounts one sub-route per action atroutes/<feature>/<verb>-<feature>.route.ts. Sub-routes useRouter({ mergeParams: true })because they are nested under/api/users/:userId/.... A route: validates the body with a Joi schema (typed to the shared DTO), calls a service, and replies viautilService. No business logic, no try/catch — wrap the handler inasyncHandlerso thrown errors reach the global handler. - Services (
services/*.service.ts) hold business logic and throwAppError(message, statusCode)for expected failures. Repositories are constructor-injected and default to shared singletons, so services are unit-testable with mocks. All services/repositories are instantiated once and exported fromservices/index.tsandrepositories/index.ts. - Repositories (
repositories/*.repository.ts) wrap TypeORM. They resolvedataSource.getRepository(...)lazily per call (so they can be built before the DataSource initializes and accept an injected DataSource in tests).
- Reply only through
utilService:replyOk/replyCreated/replyNoContent/replyError. Alwaysreturnthe call. Success payloads are sent raw (not wrapped in an envelope); errors are{ message }. - Throw
AppErrorfrom the service layer; the globalerrorHandler(mounted last inindex.ts) maps it to its status code. Anything non-AppErrorbecomes a 500.
authenticateruns globally and is best-effort: it decodes aBearerJWT intoreq.userand never rejects.requireAuthis the per-route guard that returns 401 whenreq.useris unset. It is applied to every/api/users/...mount. Tokens are JWT, 30-day expiry; passwords are bcrypt-hashed.
- Entities live in
entities/*.entity.tsand extendBaseEntity(createdAt/updatedAt/deletedAt). Use soft delete (softDelete), never harddelete. data-source.tsconfigures TypeORM (SQLite atpackages/api/pocket_pixel.sqlite); migrations are auto-discovered frommigrations/.env.tsloads.envrelative to the module (dev: package root; prod:dist/).- Per the backend-engineer skill: prefer
npm run migration:generateover hand-writing migrations after entity changes. (Note: the skill'srepo-shape.mddescribes an idealized layout —config/,redis.ts,constants/— that the current API does not all have; trust the actual tree.)
- Recurring transactions:
scheduler/recurring-scheduler.tsuses node-cron;restoreAllRecurringJobs()re-registers jobs on boot. - MCP:
.mcp.jsonexposes apocket-pixel-sqliteserver pointing at the API's SQLite DB for read/write inspection. - Deploy:
.github/workflows/ci-cd.ymlbuilds shared → runs API Jest + UI Playwright → stages a bundle → deploys to a Hostinger VPS via SSH/PM2 (ecosystem.config.js, port 4000) on push tomain.