Utility for Building Clean, Maintainable LLM Prompts with Tagged Template Literals Posted on May 29, 2025 by Dave Fowler

When building applications that interact with Large Language Models (LLMs), managing complex, conditional prompts becomes quite a mess. Langchain and others have some templating tools, but I haven't found any that resulted in a clean readable result.

So I recently made a simple solution using template literals. It's remenicent of Lit's htmlandcss utilities but is for prompts:prompt()

The Problem: Prompt Spaghetti Code

Let's start with a real example from my note-generation feature. Here's what my prompt building looked like initially:

// 5. Build the prompt
let prompt = `
You are helping a user write a note based on their own highlights and comments in a document.
Generate a short, direct note that sounds like something the user would write themselves.
Use the user's own words and style as much as possible, especially from their comments and highlighted text.
Do not summarize or describe the content; instead, write the note as if the user is 
speaking or jotting it down for themselves. Do not put it in quotes. Keep the note under 60 words.
`;

if (customInstructions) {
  prompt += `
Special instructions for this generation task:
${customInstructions}
`;
}

if (createdFromNote) {
  const docType = note2DocTypeTranslator[createdFromNote.type];
  prompt += `
==== Parent/Creating Note ====
The note was created and linked from the following ${docType}:
${note2Text(createdFromNote)}
${sourceText}
==== End of Parent/Creating Note ====
`;
}

const otherContextText = `
==== Other Context ====
${contextNotes.map((n) => note2Text(n)).join('\n\n')}
==== End of Other Context ====
`;

prompt += `
  ${otherContextText}

Now, write the user's note for "${note.title}" using their own words and style:
`;

Problems with this approach:

  • ❌ Scattered conditional logic
  • ❌ Manual header formatting repeated everywhere
  • ❌ No automatic handling of empty sections
  • ❌ Hard to read and maintain
  • ❌ Error-prone string concatenation
  • ❌ Inconsistent spacing and formatting

The Solution: Tagged Template Literals

JavaScript's tagged template literals provide a perfect foundation for building a clean prompt system. You've maybe used some html`` or css`` template literals tools before. I used the same concept to create a prompt``.

Core Template Functions

/**
 * Tagged template for conditional sections with headers.
 * Usage:
 * - prompt()`Template content here` (basic template)
 * - prompt('Title')`Template content here` (with header)
 * - prompt('Title', false)`Template content here` (with header, conditional)
 */
export const prompt = (header?: string, condition: boolean = true) => {
  return (
    strings: TemplateStringsArray,
    ...values: (string | number | undefined | null)[]
  ): string => {
    if (!condition) {
      return '';
    }

    // Build the content from template literal with full cleanup
    const content = strings
      .reduce((result, str, i) => {
        const value = values[i];
        const stringValue = value !== undefined && value !== null ? String(value) : '';
        return result + str + stringValue;
      }, '')
      .replace(/\n\s*\n\s*\n/g, '\n\n') // Collapse multiple empty lines to double newlines
      .replace(/^\s+|\s+$/g, '') // Trim leading/trailing whitespace
      .replace(/[ \t]+/g, ' ') // Normalize spaces but preserve newlines
      .replace(/\n /g, '\n') // Remove spaces after newlines
      .trim();

    if (!content) {
      return '';
    }

    if (header && header.trim()) {
      return `
==== ${header} ====
${content}
==== End of ${header} ====
`;
    }

    return content;
  };
};

The Beautiful Result

Here's the same prompt building logic as the 26 line mess above using our new template system:

const finalPrompt = prompt()`
  You are helping a user write a note based on their own highlights and comments in a document.
  Generate a short, direct note that sounds like something the user would write themselves.
  Use the user's own words and style as much as possible, especially from their comments and highlighted text.
  Do not summarize or describe the content; instead, write the note as if the user is 
  speaking or jotting it down for themselves. Do not put it in quotes. Keep the note under 60 words.

  ${prompt('Special Instructions')`${customInstructions}`}

  ${prompt('Parent/Creating Note', !!createdFromNote)`
    The note was created and linked from the following ${note2DocTypeTranslator[createdFromNote!.type]}:
    ${note2Text(createdFromNote!)}
    ${sourceText}
  `}

  ${prompt('Other Context', contextNotes.length > 0)`
    ${contextNotes.map((n) => note2Text(n)).join('\n\n')}
  `}

  Now, write the user's note for "${note.title}" using their own words and style:
`;

To me this is much more readable.

Key Benefits

Visual Clarity: The template content is right where it's used
Automatic Conditionals: Empty sections disappear automatically
Consistent Formatting: Headers are formatted consistently
Type Safety: Full TypeScript support
Composable: Easy to build complex prompts from smaller pieces
Maintainable: Changes are isolated and clear

How It Works

1. The Base prompt() Template

The main `prompt()`` template literal automatically:

  • Filters out empty/undefined values
  • Normalizes whitespace
  • Handles spacing between sections

2. Headers and Conditionals Made Simple

You may have noticed that the command is called prompt() and not prompt. The reason is that it optionally can take two simple and very useful parameters:

  • header (optional): A string header to wrap the section in formatted blocks
  • condition (optional, defaults to true): Whether to show the section or

Header formatting: When you provide a header, it automatically wraps your content:

==== YOUR HEADER HERE ====
YOUR CONTENT HERE
==== END YOUR HEADER HERE ====

Examples:

// Basic template
prompt()`Base instructions here`;

// With header
prompt('User Notes')`${notes.map((n) => n.title + '\n' + n.content).join('\n\n')}`;

// With header and condition
// (note you need to use a trailing ! to guarantee the object as Typescript won't recognize the condition check)
prompt('Optional Context', !!myObject)`Here is my object ${myObject!}`;

// Empty header = no header formatting
prompt('', true)`Just content, no header wrapper`;

3. Automatic Empty Handling

The function automatically handles empty content:

  • Empty strings become empty sections
  • Undefined/null values are filtered out
  • False conditions hide entire sections
  • Empty headers skip the header formatting

Usage Examples

Basic Conditional Section

const result = prompt()`
  Base instructions here.

  ${prompt('Optional Context')`${context}`}

  Final instructions.
`;

Why Tagged Template Literals?

JavaScript's tagged template literals are perfect for this use case because:

  1. They're native JavaScript - No external dependencies
  2. They preserve the template structure - You can see the final output clearly
  3. They support complex interpolation - Full JavaScript expressions work
  4. They're type-safe - TypeScript understands them perfectly
  5. They're composable - Easy to build larger templates from smaller ones

Conclusion

Building maintainable LLM prompts doesn't have to be painful. By leveraging JavaScript's tagged template literals, we can create clean, readable, and maintainable prompt building systems that scale with your application.

The key insights:

  • Visual proximity matters - Keep template content close to where it's used
  • Automatic handling - Let the system handle empty content and formatting
  • Leverage the platform - Tagged template literals are a perfect fit for this problem
  • Type safety - Use TypeScript to catch errors early

Try this approach in your next LLM-powered application - your future self will thank you!


What do you think? Have you built similar systems for managing complex prompts? Share your approaches in the comments!