PACE with Node.js / TypeScript
This tutorial shows how to configure PACE for a Node.js TypeScript project. FORGE will write TypeScript, GATE will run Jest or Vitest, and SENTINEL will audit for Node.js-specific security risks.
Prerequisites
- Node.js 20 LTS or later
- npm, pnpm, or yarn
- TypeScript 5+ (recommended)
- An existing project with at least a minimal test suite
- An API key for your LLM provider
Project layout assumed by this tutorial
my-api/├── pace/ ← PACE subdirectory│ └── pace.config.yaml├── src/│ ├── routes/│ ├── services/│ └── models/├── tests/ ← or src/**/*.test.ts├── package.json├── tsconfig.json└── .pace/1 — Install PACE
# From your repo rootgit clone https://github.com/pace-framework-org/pace-framework-starter pacecd pacepython -m venv .venv && source .venv/bin/activatepip install PyYAML jsonschema anthropic2 — Configure for Node.js / TypeScript
Edit pace/pace.config.yaml:
framework_version: "1.0"
product: name: "My API" description: > A Node.js TypeScript REST API built with Express. Handles user accounts, billing subscriptions, and webhook delivery for SaaS customers. github_org: "my-org"
sprint: duration_days: 14
source: dirs: - name: "src" path: "src/" language: "TypeScript" description: "Express routes, service layer, domain models, and middleware" - name: "tests" path: "tests/" language: "TypeScript" description: "Jest unit and integration tests"
tech: primary_language: "TypeScript 5 / Node.js 20" ci_system: "GitHub Actions" test_command: "npm test -- --forceExit" build_command: "npm run build" # or: npx tsc --noEmit
platform: type: local
llm: provider: anthropic model: claude-sonnet-4-6Test runner variants
| Setup | test_command |
|---|---|
| Jest (default) | npm test -- --forceExit |
| Jest (CI mode) | npx jest --ci --forceExit |
| Vitest | npx vitest run |
| Mocha + ts-node | npx mocha --require ts-node/register 'tests/**/*.test.ts' |
| pnpm + Jest | pnpm test -- --forceExit |
| Nx monorepo | npx nx test my-app |
Build command vs type-check only
If you want FORGE to catch TypeScript errors without emitting files:
tech: build_command: "npx tsc --noEmit"If you emit a compiled dist/ directory:
tech: build_command: "npm run build"3 — Set credentials
export ANTHROPIC_API_KEY="sk-ant-..."4 — Write a Node.js sprint plan
Create plan.yaml at your repo root:
release: v1.0
stories: - id: story-1 title: "Webhook entity, repository interface, and in-memory queue" status: pending acceptance_criteria: - "Webhook interface has id, url, secret, and events string[] fields" - "WebhookRepository interface has save(), findById(), and findByEvent() methods" - "InMemoryWebhookRepository implements WebhookRepository" - "Unit tests cover save-and-retrieve round-trip and findByEvent filtering" - "npm test exits 0" out_of_scope: - "HTTP delivery" - "Persistence (database)"
- id: story-2 title: "WebhookDeliveryService with HMAC-SHA256 signature" status: pending acceptance_criteria: - "deliver() sends POST with X-Webhook-Signature header" - "Signature is HMAC-SHA256 of JSON body using webhook.secret" - "Unit tests mock axios/fetch; no real HTTP calls" - "Failed deliveries (non-2xx) throw DeliveryError with status code" - "npm test exits 0"
- id: story-3 title: "Retry with exponential backoff and POST /webhooks endpoint" status: pending acceptance_criteria: - "Retries up to 3 times with 1s, 2s, 4s delays" - "POST /webhooks validates body with zod and returns 201 or 422" - "Integration test uses supertest; no real outbound HTTP" - "npm test exits 0"5 — Run Day 1
cd pacepython pace/orchestrator.py --day 1What FORGE does with TypeScript
FORGE’s tool-calling loop will:
- Read
package.json,tsconfig.json, and existing source files - Understand your module resolution, import aliases, and existing patterns
- Write
.tsfiles using interfaces, classes, and async/await patterns - Run
npx tsc --noEmit(orbuild_command) to catch type errors - Run
npm testand read Jest output to self-correct failures
A typical FORGE run for Day 1:
[FORGE] Reading package.json ...[FORGE] Reading tsconfig.json ...[FORGE] Reading src/index.ts ...[FORGE] Writing src/webhooks/Webhook.ts ...[FORGE] Writing src/webhooks/WebhookRepository.ts ...[FORGE] Writing src/webhooks/InMemoryWebhookRepository.ts ...[FORGE] Writing tests/webhooks/InMemoryWebhookRepository.test.ts ...[FORGE] Running build: npx tsc --noEmit ...[FORGE] Running tests: npm test -- --forceExit ...[FORGE] All tests pass. Calling complete_handoff.What SENTINEL checks for Node.js
SENTINEL applies Node.js-specific checks:
- Injection: NoSQL injection in MongoDB queries, template literal injection in SQL
- Prototype pollution: unsafe use of
Object.assignor lodashmergewith user input - Path traversal:
path.joinorfs.readFilewith unvalidated user-provided paths - Secrets: API keys or tokens hardcoded in source,
.envfiles committed - Dependency risks:
eval(),Function()constructor with dynamic strings,child_process.execwith user input - HTTP security: missing
helmet, missing rate limiting on auth endpoints - JWT: weak secrets, missing expiry validation,
nonealgorithm acceptance
6 — NestJS projects
NestJS projects work the same way with a small config adjustment:
source: dirs: - name: "src" path: "src/" language: "TypeScript" description: > NestJS application. Uses decorators, modules, and dependency injection. Controllers in src/*/**.controller.ts, services in src/*/**.service.ts.
tech: primary_language: "TypeScript 5 / Node.js 20 / NestJS 10" test_command: "npm run test -- --forceExit" build_command: "npm run build"Include a note about NestJS patterns in product.description so FORGE generates proper @Injectable(), @Controller(), and @Module() decorators.
7 — Monorepo setups (Turborepo / Nx)
source: dirs: - name: "webhook-service" path: "apps/webhook-service/src/" language: "TypeScript" description: "Webhook delivery microservice — NestJS, Prisma ORM"
tech: test_command: "npx turbo test --filter=webhook-service" build_command: "npx turbo build --filter=webhook-service"8 — FORGE hints for TypeScript
Add your conventions to product.description so FORGE generates strictly-typed, lint-clean code from the start:
product: description: > A Node.js TypeScript REST API built with Express. Use strict TypeScript — no any types; use unknown for untyped values. Interfaces in src/interfaces/, implementations injected via constructor. All async operations return Promise<T>. Use jest-mock-extended for interface mocks in tests — never jest.mock() for DI-injected dependencies. All files use ES module imports with .js extensions (TypeScript resolves them). Max 100 chars per line. Code must pass ESLint with typescript-eslint strict rules.For NestJS projects, add:
product: description: > ... NestJS app with decorators. Use @Injectable() on every provider. All modules explicit — no global providers outside CoreModule. Use ConfigService for env vars, never process.env directly.9 — Test coverage (Jest)
Configure coverage thresholds in jest.config.js:
/** @type {import('jest').Config} */module.exports = { preset: 'ts-jest', testEnvironment: 'node', collectCoverageFrom: ['src/**/*.ts', '!src/**/*.d.ts'], coverageThreshold: { global: { lines: 80, functions: 80, branches: 70, }, },};Update test_command to collect coverage:
tech: test_command: "npm test -- --forceExit --coverage"Jest exits non-zero when thresholds are not met — GATE will HOLD until FORGE reaches them.
For Vitest:
export default defineConfig({ test: { coverage: { provider: 'v8', thresholds: { lines: 80, functions: 80 }, }, },});10 — Linter and formatter (ESLint + Prettier)
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettierbuild_command runs ESLint and type-checking:
tech: build_command: "npx eslint src/ --max-warnings 0 && npx tsc --noEmit" test_command: "npm test -- --forceExit"eslint.config.js with typescript-eslint strict preset:
import tseslint from 'typescript-eslint';
export default tseslint.config( tseslint.configs.strict, { rules: { '@typescript-eslint/no-explicit-any': 'error' } });FORGE hint: "Code must pass ESLint with no warnings. No any types allowed."
11 — Database testing patterns
jest-mock-extended (type-safe interface mocks)
npm install --save-dev jest-mock-extendedimport { mock } from 'jest-mock-extended';import { WebhookRepository } from '../src/webhooks/WebhookRepository';
const mockRepo = mock<WebhookRepository>();mockRepo.findById.mockResolvedValue({ id: '1', url: 'https://example.com' });FORGE hint: "Use jest-mock-extended for all repository interface mocks."
pg-mem (in-memory PostgreSQL)
import { newDb } from 'pg-mem';
const db = newDb();const pool = db.adapters.createPg();const repo = new PostgresWebhookRepository(pool);Testcontainers
import { PostgreSqlContainer } from '@testcontainers/postgresql';
let container: StartedPostgreSqlContainer;
beforeAll(async () => { container = await new PostgreSqlContainer().start(); process.env.DATABASE_URL = container.getConnectionUri();});
afterAll(() => container.stop());Mark integration tests with --testPathPattern and exclude from the default test_command.
12 — Mock patterns
jest.fn() with TypeScript generics
const sendEmail = jest.fn<Promise<void>, [string, string]>();sendEmail.mockResolvedValue(undefined);
await notifyUser('user@example.com', 'Welcome');
expect(sendEmail).toHaveBeenCalledWith('user@example.com', expect.stringContaining('Welcome'));Spying on module exports
import * as mailer from '../src/mailer';const spy = jest.spyOn(mailer, 'sendEmail').mockResolvedValue(undefined);FORGE hint: "Prefer constructor-injected interfaces over module-level mocking. Only use jest.spyOn for third-party libraries without interfaces."
13 — Multi-day sprint (Day 2 builds on Day 1)
After Day 1 ships, SCRIBE records types and interfaces in engineering.md. Day 2’s FORGE reads it:
[FORGE] Reading .pace/context/engineering.md ...[FORGE] Found: src/webhooks/Webhook.ts (Webhook interface, WebhookRepository interface)[FORGE] Found: src/webhooks/InMemoryWebhookRepository.ts (implements WebhookRepository)[FORGE] Writing src/webhooks/WebhookDeliveryService.ts ...[FORGE] Writing tests/webhooks/WebhookDeliveryService.test.ts ...Story-2 criteria can reference WebhookRepository by name — FORGE already knows its methods.
14 — Advisory clearance story
- id: story-5 title: "Security and DevOps advisory clearance" status: pending acceptance_criteria: - "All open SENTINEL advisories resolved or escalated with written justification" - "All open CONDUIT advisories resolved or escalated" - "npm test exits 0 after any remediation changes"Common Node.js SENTINEL advisories:
eval()ornew Function()with dynamic strings — fix: replace with a safe alternative or sandboxed executionchild_process.exec(cmd)with user-controlledcmd— fix: useexecFilewith an argument array- Missing
helmet()middleware — fix: addapp.use(helmet())in Express app setup .envfile committed to git — fix: add to.gitignore, use environment secrets in CI
15 — CI build caching
Cache npm or pnpm packages to speed up PACE runs:
# npm- uses: actions/setup-node@v4 with: node-version: '20' cache: 'npm'
# pnpm- uses: pnpm/action-setup@v3 with: version: 9- uses: actions/setup-node@v4 with: node-version: '20' cache: 'pnpm'For monorepos, set cache-dependency-path to the specific package-lock.json or pnpm-lock.yaml.
16 — Windows notes
npm test and npx work natively on Windows. For cross-platform env vars in package.json scripts, use cross-env:
"scripts": { "test": "cross-env NODE_ENV=test jest --forceExit"}Path separators in source.dirs use forward slashes — Node.js normalises them on Windows.
Common issues
Jest hangs after tests pass
Add --forceExit to your test command. If you use database connections or an Express server in tests, ensure afterAll() closes them — FORGE will respect this pattern if you note it in product.description.
TypeScript path aliases not resolved in Jest
If you use paths in tsconfig.json (e.g. @/services/*), ensure moduleNameMapper in jest.config.js mirrors them. FORGE reads jest.config.js and will use the same aliases in new test files.
build_command fails with module not found
If npx tsc cannot find a type definition, add the missing @types/* package to devDependencies manually, then re-run the day. FORGE can only write code it believes compiles; it cannot add npm packages.
ESM / CommonJS module conflicts
If your project mixes ESM and CJS, include a clear note in product.description: “The project uses ESM ("type": "module" in package.json). All imports must use file extensions.”