Skip to content

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

Terminal window
# From your repo root
git clone https://github.com/pace-framework-org/pace-framework-starter pace
cd pace
python -m venv .venv && source .venv/bin/activate
pip install PyYAML jsonschema anthropic

2 — 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-6

Test runner variants

Setuptest_command
Jest (default)npm test -- --forceExit
Jest (CI mode)npx jest --ci --forceExit
Vitestnpx vitest run
Mocha + ts-nodenpx mocha --require ts-node/register 'tests/**/*.test.ts'
pnpm + Jestpnpm test -- --forceExit
Nx monoreponpx 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

Terminal window
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

Terminal window
cd pace
python pace/orchestrator.py --day 1

What FORGE does with TypeScript

FORGE’s tool-calling loop will:

  1. Read package.json, tsconfig.json, and existing source files
  2. Understand your module resolution, import aliases, and existing patterns
  3. Write .ts files using interfaces, classes, and async/await patterns
  4. Run npx tsc --noEmit (or build_command) to catch type errors
  5. Run npm test and 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.assign or lodash merge with user input
  • Path traversal: path.join or fs.readFile with unvalidated user-provided paths
  • Secrets: API keys or tokens hardcoded in source, .env files committed
  • Dependency risks: eval(), Function() constructor with dynamic strings, child_process.exec with user input
  • HTTP security: missing helmet, missing rate limiting on auth endpoints
  • JWT: weak secrets, missing expiry validation, none algorithm 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:

vitest.config.ts
export default defineConfig({
test: {
coverage: {
provider: 'v8',
thresholds: { lines: 80, functions: 80 },
},
},
});

10 — Linter and formatter (ESLint + Prettier)

Terminal window
npm install --save-dev eslint @typescript-eslint/eslint-plugin @typescript-eslint/parser prettier

build_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)

Terminal window
npm install --save-dev jest-mock-extended
import { 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() or new Function() with dynamic strings — fix: replace with a safe alternative or sandboxed execution
  • child_process.exec(cmd) with user-controlled cmd — fix: use execFile with an argument array
  • Missing helmet() middleware — fix: add app.use(helmet()) in Express app setup
  • .env file 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.”