Prompt engineering for developers: a practical checklist

A prompt engineering checklist for developers shipping LLM features: role, context, constraints, examples, output format and evaluation, with TypeScript code.

By Hamza Ghouri

Prompt engineering has a reputation for being part magic, part superstition. For developers building product features, it is closer to writing a good function signature and a good ticket at the same time. You are telling a system what it is, what it knows, what it may not do, and what shape the answer must take. Then you test it.

This is the checklist I run through when I write a prompt that will sit inside a production feature. It applies whether the feature is a summariser, a classifier, or a multi-step pipeline like the one behind REQ Agent, my CLI that turns raw client discovery notes into specs and handoff docs.

1. Role: say what the model is doing

One or two sentences that frame the task. "You are a senior backend engineer reviewing an API spec for missing error cases" gives the model a vocabulary and a level of detail to aim for. Long personas add little. Be specific about the job and the audience of the output.

Checks:

  • Does the role match who will read the output (a developer, a client, an end user)?
  • Is the task stated as one clear job?

If you find yourself describing three jobs, you probably want three calls. I go into that in prompt chaining vs one giant prompt.

2. Context: give it what it cannot guess

The model only knows what is in the prompt plus its general training. Your product's rules, your data, and your definitions have to be supplied.

  • Put reference material in clearly delimited blocks (<document>...</document> or fenced sections) so the model can tell instructions from data.
  • Include only what is relevant. Dumping an entire knowledge base in hurts accuracy and costs tokens.
  • Define your terms. If "active user" means something specific in your product, say so.
  • Treat user-supplied content as data. Anything a user can type can contain instructions, so keep it inside delimiters and never let it override your system rules. Prompt injection is a real attack surface in every LLM feature.

3. Constraints: say what must and must not happen

Constraints are where most production prompts are too vague.

  • Length limits in concrete terms ("at most 5 bullet points", "under 80 words").
  • Scope limits ("only use information from the provided document; if the answer is missing, say so").
  • Tone and audience ("plain English, no jargon, written for a non-technical founder").
  • Hard rules ("never include prices", "never invent feature names that are absent from the input").

Write constraints positively where you can. "Answer only from the document" is clearer than a list of things to avoid.

4. Examples: show the pattern

One to three examples of input and ideal output do more than paragraphs of description. A few guidelines:

  • Make examples representative of real inputs, including a messy one.
  • Vary them, or the model will copy surface details like length and wording.
  • Include an edge case example, such as an input where the right answer is "not enough information".

Keep examples in the codebase next to the prompt, in version control, so changes are reviewed like code.

5. Output format: make it machine-checkable

If code consumes the output, ask for structured output and validate it. My default is JSON matched against a Zod schema, with a retry or a clear failure when it does not parse.

import { z } from "zod";

const Triage = z.object({
  category: z.enum(["bug", "feature", "question"]),
  priority: z.enum(["low", "medium", "high"]),
  summary: z.string().max(200),
});

async function callModel(system: string, user: string): Promise<string> {
  const res = await fetch(process.env.LLM_API_URL!, {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${process.env.LLM_API_KEY}`,
    },
    body: JSON.stringify({ system, input: user }),
  });
  if (!res.ok) throw new Error(`Model API error ${res.status}`);
  const data = await res.json();
  return data.output;
}

export async function triageTicket(ticket: string) {
  const system = [
    "You triage support tickets for a SaaS product.",
    "Return only JSON with keys: category, priority, summary.",
    "category is one of bug, feature, question.",
    "priority is one of low, medium, high.",
    "summary is one sentence, under 200 characters.",
  ].join("\n");

  const raw = await callModel(system, `<ticket>\n${ticket}\n</ticket>`);
  return Triage.parse(JSON.parse(raw));
}

The request body shape here is a placeholder; adapt callModel to your provider. The pattern that matters is the schema and the parse step. For more on this, see getting reliable JSON from LLMs.

6. Evaluation: test the prompt like code

A prompt you have only tried on three inputs is untested. Before shipping:

  • Build a small set of real inputs with expected outputs or grading criteria. Twenty to fifty cases is a reasonable start.
  • Include the hard ones: empty input, very long input, off-topic input, adversarial input.
  • Re-run the set every time you change the prompt or switch models. Small wording changes can move results in surprising directions.
  • Track failures by type so you know whether to fix the prompt, the context, or the validation.

REQ Agent validates its generated docs for structure, feature coverage, and consistency between design tokens and the dev handoff before writing anything. That validation step is a form of evaluation that runs on every single output, and I'd recommend some version of it for any feature where wrong output is costly.

The checklist, condensed

  • Role: one clear job, matched to the reader.
  • Context: only what is relevant, delimited, with terms defined and user input treated as data.
  • Constraints: concrete limits, scope rules, hard rules.
  • Examples: a few varied, realistic ones, including an edge case.
  • Output format: structured, schema-validated, with a failure path.
  • Evaluation: a versioned test set you re-run on every change.

Where I'd start

Pick one prompt already in your product and add the two cheapest items: a schema check on the output and a twenty-case test set. Those two changes usually reveal more about prompt quality than any amount of rewording, and they make every future change safer.

Want this kind of work on your product?

Get in touch