# Prompt Engineering for Engineers: Treating Prompts Like API Contracts

The mistake I see backend engineers make when they first start writing prompts is treating the exercise like writing a comment, informal, whatever gets the point across. Then the prompt drifts as the product evolves, nobody remembers why a particular phrase is in there, and the whole thing becomes exactly the kind of unversioned, untested, unreviewed artifact we'd never accept for actual application code. Treating a prompt like an API contract, not a comment, is what fixed that for me.

### A prompt has inputs, outputs, and a contract, just like an endpoint

typescript

```typescript
interface FraudCheckPromptInput {
  transactionAmount: number;
  merchantCategory: string;
  customerHistory: TransactionSummary[];
}

interface FraudCheckPromptOutput {
  riskScore: number;
  reasoning: string;
  recommendation: "approve" | "review" | "decline";
}
```

Defining these types before writing a single line of prompt text changes the exercise entirely. You're not asking "what words get a good response," you're asking "what contract does this prompt need to fulfill," which is a question every backend engineer already knows how to answer.

### Version your prompts like you version your API

typescript

```typescript
const FRAUD_CHECK_PROMPT_V3 = `
You are assessing transaction risk. Given the transaction details and 
customer history, respond with a JSON object matching this exact shape:
{ "riskScore": number (0-1), "reasoning": string, "recommendation": "approve" | "review" | "decline" }
`;
```

A prompt embedded as an inline string, edited in place every time someone tweaks it, is the prompt equivalent of hotfixing production without a deploy log. I keep prompts in version control with the same discipline as code, including a changelog entry explaining why a specific version changed, because "we changed the wording and false positives went up" is a debugging question you will eventually need to answer.

### Test prompts the way you test business logic

typescript

```typescript
describe("fraud check prompt", () => {
  it("flags high-value transactions from new customers", async () => {
    const result = await runFraudCheck({
      transactionAmount: 50000,
      merchantCategory: "electronics",
      customerHistory: [],
    });
    expect(["review", "decline"]).toContain(result.recommendation);
  });
});
```

These tests are probabilistic in a way unit tests for deterministic code aren't, the same prompt can produce slightly different phrasing across runs. What you're testing isn't exact output, it's that the contract holds: the shape validates, and the recommendation falls within an acceptable range for a clearly risky input. I maintain a small set of golden test cases, inputs with known-correct expected categories of output, and run them against every prompt change before it ships.

### Explicit constraints beat implicit hope

Early prompts I wrote said things like "keep your response concise." Vague instructions produce vague compliance. Being explicit, "respond in valid JSON only, no markdown formatting, no explanation text outside the JSON object", produces dramatically more consistent output than a soft suggestion, because you're giving the model an actual specification instead of a vibe.

### Prompts drift in behavior even when the text doesn't change

This is the one that catches experienced engineers off guard, because it violates an assumption we bring from regular code: the same input to the same function producing the same output. A model provider update can shift how your unchanged prompt behaves. I log prompt version alongside model version and response, specifically so a shift in failure rate can be traced to "the model changed under us" rather than assumed to be a code bug.

### The real discipline

A prompt is a contract with a probabilistic system, and it deserves the same rigor we'd apply to any other contract: versioning, testing, explicit constraints, and monitoring for drift. Treating it as a casual string you tweak until it feels right is how a working feature becomes an unmaintainable one within two model updates.
