Skip to content
Prompting Press v0.2

Getting started — TypeScript

Terminal window
npm install prompting-press zod

ESM-only · Node 20+ · ships as a native addon (per-platform binary).

zod is a peer dependency for the typed-validation facade (the consuming project supplies its own Zod version). A Zod schema is passed at render time; the library imports only the structural safeParse interface, not Zod’s identity, so any object exposing safeParse works.

A prompt definition is YAML, JSON, or TOML text — the same shape in any of the three formats. The caller reads the text and hands it to the from* factories; the library does no file I/O. The assistant definition used throughout this page:

assistant.yaml
name: assistant
role: system
body: "You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words."
variables:
company:
type: string
trusted: true
max_words:
type: integer
trusted: true

Step 1 — construct a Prompt (and validate)

Section titled “Step 1 — construct a Prompt (and validate)”

Read the definition file and hand its text to a from* factory (fromYaml / fromJson / fromToml), or build from a typed PromptDefinition object with new Prompt(...). The library does no file I/O — the caller reads the file. Both routes run the same validation immediately:

getting-started_typescript_construct_from_text.ts
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { Prompt } from "prompting-press";
// The caller reads the definition; the library does no file I/O itself.
// Resolve the file next to this program (a real app uses its own path).
const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
test("construct from a file", () => {
const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8")); // validates here, or throws
// The same definition in JSON or TOML parses into an identical Prompt:
// const assistant = Prompt.fromJson(readFileSync(defFile("assistant.json"), "utf8"));
// const assistant = Prompt.fromToml(readFileSync(defFile("assistant.toml"), "utf8"));
assert.equal(assistant.name, "assistant");
assert.equal(
assistant.body,
"You are a support assistant for {{ company }}. Keep your replies under {{ max_words }} words.",
);
});

Construction validates immediately: had the body referenced {{ industry }} — a variable variables doesn’t declare — construction would have thrown a PromptRenderError, before any render (malformed text throws LoadError). A template/variable disagreement is caught at construction, never as a silent empty render.

The render values are a caller-owned Zod schema whose keys match the prompt’s variables (company, max_words). This schema is the render-time gate: you hand it to render in Step 3, and its rules (here max_words must be a positive integer) decide what data is allowed to reach the kernel. Declaring it is just the definition — the enforcement happens at render:

getting-started_typescript_declare_vars.ts
import assert from "node:assert/strict";
import { test } from "node:test";
import { z } from "zod";
// The render values are a caller-owned Zod schema. Its keys match the prompt's
// `variables` (`company`, `max_words`), and `safeParse` runs before the kernel is touched.
const AssistantVars = z.object({
company: z.string().min(1),
max_words: z.number().int().min(1),
});
test("the vars schema validates matching data", () => {
const parsed = AssistantVars.safeParse({ company: "Acme Robotics", max_words: 50 });
assert.ok(parsed.success);
});

Step 3 — render (the schema validates the data), and read the result

Section titled “Step 3 — render (the schema validates the data), and read the result”

Hand render the AssistantVars schema plus the data. The schema runs first — valid data renders; invalid data (e.g. max_words: 0) throws PromptValidationError and the kernel is never touched. The sample shows both the passing render and the rejection:

getting-started_typescript_render_and_read.ts
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { Prompt, PromptValidationError } from "prompting-press";
import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.
// Resolve the file next to this program (a real app uses its own path).
const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({
company: z.string().min(1),
max_words: z.number().int().min(1),
});
const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
test("render, and read the result", () => {
const result = assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 50 });
assert.equal(result.text, "You are a support assistant for Acme Robotics. Keep your replies under 50 words."); // => "You are a support assistant for Acme Robotics. Keep your replies under 50 words."
assert.equal(result.variant, "default"); // => "default" (same arm assistant.body showed in Step 1)
assert.match(result.templateHash, /^[0-9a-f]{64}$/); // 64-char lowercase-hex SHA-256 of the template
assert.match(result.renderHash, /^[0-9a-f]{64}$/); // 64-char lowercase-hex SHA-256 of result.text
assert.equal(result.guard, null); // => null (no guard requested)
});
test("render validates the data through the schema — bad data is rejected before the kernel", () => {
// max_words: 0 violates AssistantVars (.int().min(1)); render throws, nothing is rendered.
assert.throws(
() => assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 0 }),
PromptValidationError,
);
});

render has two forms, and the optional variant/guard config rides in a trailing opts object:

prompt.render(schema, data, opts?); // schema form: Zod validates data first, then renders
prompt.render(data, opts?); // static form: already-typed data, marshaled directly
// opts: { variant?: string; guard?: GuardConfig | null }

The schema form is the one to reach for by default — it is what makes the typed-vars validation above actually run. variant absent selects the default (root body) arm; guard absent opts out of the advisory guard.

getting-started_typescript_complete_example.ts
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import { Prompt } from "prompting-press";
import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.
// Resolve the file next to this program (a real app uses its own path).
const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({
company: z.string().min(1),
max_words: z.number().int().min(1),
});
test("complete example", () => {
// 1. Construct from the definition file (validates here).
const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
// 2 + 3. Render with the typed, Zod-validated vars.
const result = assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 50 });
console.log(result.text); // You are a support assistant for Acme Robotics. Keep your replies under 50 words.
console.log(result.templateHash); // 64-char hex
assert.equal(result.text, "You are a support assistant for Acme Robotics. Keep your replies under 50 words.");
assert.match(result.templateHash, /^[0-9a-f]{64}$/);
});
FieldValue
textThe rendered body text.
nameThe prompt name.
variantThe resolved variant name ("default" when none was selected).
templateHashSHA256(resolved variant template source) — lowercase hex.
renderHashSHA256(rendered output text) — lowercase hex.
guard`string
getting-started_typescript_error_types.ts
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { fileURLToPath } from "node:url";
import { test } from "node:test";
import {
Prompt,
PromptingPressError,
PromptValidationError,
PromptRenderError,
LoadError,
} from "prompting-press";
import { z } from "zod";
// The caller reads the definition; the library does no file I/O itself.
// Resolve the file next to this program (a real app uses its own path).
const defFile = (name: string) => fileURLToPath(new URL(name, import.meta.url));
const AssistantVars = z.object({
company: z.string().min(1),
max_words: z.number().int().min(1),
});
const assistant = Prompt.fromYaml(readFileSync(defFile("assistant.yaml"), "utf8"));
test("a rejected render surfaces a structured PromptValidationError", () => {
let caught = false;
try {
assistant.render(AssistantVars, { company: "Acme Robotics", max_words: 0 });
} catch (err) {
if (err instanceof PromptValidationError) {
caught = true;
for (const row of err.errors) {
console.error(row.field, row.code, row.message);
// "max_words" "validation" "Too small: expected number to be >=1"
}
assert.equal(err.errors[0]?.field, "max_words");
assert.equal(err.errors[0]?.code, "validation");
}
}
assert.ok(caught, "expected a PromptValidationError");
// The error hierarchy: every type extends the base, which extends Error.
assert.ok(PromptValidationError.prototype instanceof PromptingPressError);
assert.ok(PromptRenderError.prototype instanceof PromptingPressError);
assert.ok(LoadError.prototype instanceof PromptingPressError);
assert.ok(PromptingPressError.prototype instanceof Error);
});

The error hierarchy (all extend Error):

PromptingPressError # base; carries .errors: FieldError[]
├── PromptValidationError # Zod validation failed (code "validation")
├── PromptRenderError # kernel render failure (codes: unknown_variant,
│ # undefined_variable, parse, render, excluded_feature)
└── LoadError # malformed YAML/JSON/TOML (code "load")

Each FieldError carries field, code, and message. A rejected render value is never echoed onto the error surface; a parse error carries template-syntax detail (no bound values).

docs current as of 0.2.0