A full-stack project
A typical project: shop-api (nest-api), shop-web (react-app), shop-worker
(node-worker), and maybe shop-py (py-worker). Each is its own repository, and
the defaults already agree on the local ports and services.
Generate
Section titled “Generate”stack new nest-api ~/code/shop/api --name shop-apistack new react-app ~/code/shop/web --name shop-webstack new node-worker ~/code/shop/worker --name shop-workerAdd --with temporal to the API and the worker if you need durable workflows.
The links, one by one
Section titled “The links, one by one”Front end → API
Section titled “Front end → API”| Front end | API |
|---|---|
VITE_APP_API_URL=http://localhost:5000 |
listens on PORT=5000 |
its origin, http://localhost:3000 |
FRONTEND_HOST=http://localhost:3000 |
calls /auth/* for sessions |
better-auth at /auth |
yarn gen reads /openapi-json with API_DOC_USER/_PASSWORD |
BASIC_AUTH_USER/BASIC_AUTH_PASS |
Regenerate the front end’s types whenever the API’s endpoints change, and commit them: they’re the contract, and a stale one fails at compile time rather than at runtime.
API ↔ worker: jobs
Section titled “API ↔ worker: jobs”The API produces jobs; the worker consumes them. Both sides need the same Redis and the same queue name.
// API: produce (in any module)@InjectQueue('reports') private readonly reports: Queueawait this.reports.add('monthly', { accountId }, { attempts: 5, backoff: { type: 'exponential', delay: 10_000 } });
// API: register the queue in that module's importsQueueModule.register('reports')export const REPORTS_QUEUE = 'reports';// worker: src/workers/reports/index.tsexport const reports = createWorker<{ accountId: string }>(REPORTS_QUEUE, async (job) => { … });// worker: src/index.tsconst queueWorkers = [Workers.example, Workers.reports];Retries and backoff are set by the producer (attempts, backoff); the
worker throws to fail a job.
API ↔ worker: data
Section titled “API ↔ worker: data”The worker uses the API’s database directly. It needs the API’s schema:
- node-worker: copy the API’s
prisma/schema/*.prismainto the worker’sprisma/schema/(replacing the placeholderUser), or better, make the API’sprisma/schemaa git submodule of the worker. Runyarn db:generateafter every schema change, and redeploy. - py-worker: hand-write SQLAlchemy models matching the tables it uses.
Only the API runs migrations.
API → Temporal → worker
Section titled “API → Temporal → worker”The API starts workflows by name on a task queue; a worker polling that queue
runs them. Same TEMPORAL_ADDRESS and TEMPORAL_NAMESPACE everywhere, and the
API’s TEMPORAL_TASK_QUEUE must be one a worker polls. See
Temporal.
Everything → OpenObserve
Section titled “Everything → OpenObserve”One organization per project; each service a distinct OTEL_SERVICE_NAME
(shop-api, shop-worker, shop-web), so each has its own stream and a trace
can cross all of them. See Observability end to end.
Run it all locally
Section titled “Run it all locally”- API:
yarn dc:up && yarn dc:wait,yarn db:migrate,yarn db:seed,yarn start. - Worker:
yarn db:generate && yarn dev. - Web:
yarn dev, then openhttp://localhost:3000and sign in.