Psychology quizzes are everywhere online, and most of them are made up. When I built Ask.Dr — a site with 240+ free psychology self-assessments, symptom guides and articles — the core rule was that clinical-style screeners should use published, validated instruments, implemented exactly as specified, with clear "this is not a diagnosis" framing.
The PHQ-9 depression questionnaire (Kroenke, Spitzer & Williams, 2001) is a good example of what that involves in code.
Model the instrument, not just the questions
A validated screener is more than a list of questions. It has a fixed response scale, a scoring rule, severity bands and, sometimes, items that need special handling. Put all of that in data:
interface Instrument {
id: string;
citation: string;
timeframe: string; // "Over the last 2 weeks..."
options: { label: string; value: number }[];
items: { id: string; text: string; flag?: "safety" }[];
bands: { min: number; max: number; label: string }[];
}
const phq9Bands = [
{ min: 0, max: 4, label: "Minimal" },
{ min: 5, max: 9, label: "Mild" },
{ min: 10, max: 14, label: "Moderate" },
{ min: 15, max: 19, label: "Moderately severe" },
{ min: 20, max: 27, label: "Severe" },
];
Each of the 9 items is answered on a 0–3 scale ("Not at all" to "Nearly every day"), so the total ranges from 0 to 27.
Score strictly — and refuse partial results
Validated cut-offs only mean something when every item is answered. Don't silently treat a skipped item as zero:
export function score(inst: Instrument, answers: Record<string, number | undefined>) {
const missing = inst.items.filter((i) => answers[i.id] === undefined);
if (missing.length) return { ok: false as const, missing: missing.map((m) => m.id) };
const total = inst.items.reduce((sum, i) => sum + (answers[i.id] as number), 0);
const band = inst.bands.find((b) => total >= b.min && total <= b.max)!;
return { ok: true as const, total, band: band.label };
}
Write tests for every band boundary (4/5, 9/10, 14/15, 19/20). Off-by-one errors here change what a person is told about their mental health.
Safety items override the score
The last PHQ-9 item asks about thoughts of being better off dead or of self-harm. Any non-zero answer on that item matters regardless of the total score. Someone can score "Mild" overall and still need to see crisis resources immediately.
export function needsSafetyMessage(inst: Instrument, answers: Record<string, number>) {
return inst.items.some((i) => i.flag === "safety" && (answers[i.id] ?? 0) > 0);
}
In the UI, the safety message renders above the score, not below it, and it's never hidden behind a click.
Frame results honestly
Validated doesn't mean diagnostic. A few rules for the results page:
- Say plainly that it's a screening tool, not a diagnosis, therapy or emergency care.
- Show the citation, so people can see where the instrument comes from.
- Describe the band in neutral language and suggest talking to a professional for moderate or higher scores.
- Don't store answers server-side unless you truly need to — these are sensitive data.
Keep the content reviewable
Alongside the tests, symptom guides and blog articles use plain language and are reviewed for accuracy. Keeping instruments as data (rather than hardcoded components) makes that review much easier: a reviewer can check one file against the original paper.
Takeaways
- Implement published instruments exactly — items, scale, scoring, bands.
- Refuse to score incomplete responses.
- Treat safety items as overrides, not as part of a sum.
- Be explicit that screeners aren't diagnoses, and point people to real help.
The tests are free at askdr.app. If you've built health-related tools, I'd like to hear how you handled review and safety messaging.
If you're struggling right now, please reach out to a local crisis line or emergency services.
Top comments (0)