Post image


AI is here. That might seem like a trite comment, but almost a quarter of developers still see AI as something they don’t plan to use:

Post image

But ‘using AI’ doesn’t necessarily mean vibe coding your application into oblivion. Using AI as a developer means two things:

  1. Understanding that AI is an ideal pair-programming partner
  2. Understanding how to get the most out of AI to create the code you want

The key to the second is effective prompt engineering. Along with programming principles like DRY, SOLID, and other development best practices, prompt engineering is emerging as a critical skill in the modern developer’s toolkit. Great code from LLMs begins with great prompts. Just as writing clean functions or classes requires care and structure, crafting effective prompts demands methodical thinking and precision.

Prompting is not a guessing game—it’s a craft rooted in logic, testing, and structure. The most successful developers approach prompts with the same rigor they bring to traditional code: designing, refining, and optimizing for clear outputs.

Here, we argue that developers should treat prompts as software components—modular, testable pieces that can be evaluated, iterated on, and integrated into larger systems. When viewed through this lens, prompt engineering becomes a systematic discipline, allowing developers to harness AI with consistency and confidence.

Few-Shot and One-Shot Prompting: Show, Don’t Just Tell

When you provide examples of the output you want, you increase the likelihood of receiving properly formatted, contextually appropriate code. This approach leverages the language model’s pattern-matching abilities.

Without an example:

Write a function to calculate the Fibonacci sequence.

Output:

Here is a Python function to calculate the Fibonacci sequence up to a specified number of terms:
```python
def fibonacci(n):
    fib_sequence = [0, 1]
    for i in range(2, n):
        next_number = fib_sequence[i-1] + fib_sequence[i-2]
        fib_sequence.append(next_number)
    return fib_sequence
# Example usage
n = 10
result = fibonacci(n)
print(result)
```
This function generates the Fibonacci sequence up to the nth term and returns a list containing the sequence. You can specify the value of `n` to generate the sequence up to that term.

With an example:

Write a function to calculate the Fibonacci sequence.

Example of another function with my preferred style:
```python
def is_prime(n: int) -> bool:
    """
    Determines if a number is prime.
    
    Args:
        n: The number to check
        
    Returns:
        True if n is prime, False otherwise
    """
    if n <= 1:
        return False
    if n <= 3:
        return True
    if n % 2 == 0 or n % 3 == 0:
        return False
    i = 5
    while i * i <= n:
        if n % i == 0 or n % (i + 2) == 0:
            return False
        i += 6
    return True

Output:

def fibonacci(n: int) -> List[int]:
    """
    Generates the Fibonacci sequence up to the nth number.
    
    Args:
        n: The number of Fibonacci numbers to generate
        
    Returns:
        A list of Fibonacci numbers up to the nth number
    """
    fib_sequence = []
    a, b = 0, 1
    for _ in range(n):
        fib_sequence.append(a)
        a, b = b, a + b
    return fib_sequence

With the example, the model mirrors your preferred documentation style and function signature conventions. Instead of assuming defaults, it adapts to the structure you’ve provided, producing more idiomatic and integration-ready code.

Chain-of-Thought: Induce Stepwise Reasoning

By prompting the AI to work through a problem step-by-step, you can ensure logical progression and catch potential issues before they manifest in code. This pattern is particularly valuable for complex algorithms or business logic.

With no reasoning:

Create a function that implements quicksort for an array of integers.

Output:

Sure! Here is an implementation of the quicksort algorithm in Python:
```python
def quicksort(arr):
    if len(arr) <= 1:
        return arr
    else:
        pivot = arr[0]
        less_than_pivot = [x for x in arr[1:] if x <= pivot]
        greater_than_pivot = [x for x in arr[1:] if x > pivot]
        return quicksort(less_than_pivot) + [pivot] + quicksort(greater_than_pivot)
# Example usage
arr = [3, 6, 8, 10, 1, 2, 1]
sorted_arr = quicksort(arr)
print(sorted_arr)
```
This function takes an array of integers as input and returns a sorted array using the quicksort algorithm.

With reasoning:

Create a function that implements quicksort for an array of integers.

Please:
First explain the quicksort algorithm and its time complexity
Then outline the key components needed in the implementation
Write the function with clear, descriptive variable names
Add appropriate error handling
Include comments explaining each major step

Output:

### Quicksort Algorithm:
- Quicksort is a popular sorting algorithm that uses the divide-and-conquer strategy.
- It works by selecting a pivot element from the array and partitioning the other elements into two sub-arrays according to whether they are less than or greater than the pivot.
- The process is then repeated recursively for the sub-arrays.
- The average case time complexity of quicksort is O(n log n), but it can degrade to O(n^2) in the worst case.
### Key Components for Implementation:
1. Choose a pivot element (commonly the last element in the array).
2. Partition the array around the pivot such that elements less than pivot are on the left and elements greater are on the right.
3. Recursively apply the above steps to the sub-arrays.
### Function Implementation:
```python
from typing import List
def quicksort(arr: List[int]) -> List[int]:
    """
    Sorts an array of integers using the quicksort algorithm.
    Args:
    arr (List[int]): List of integers to be sorted
    Returns:
    List[int]: Sorted list of integers
    """
    if len(arr) <= 1:
        return arr  # Base case: if array is empty or has one element, it is already sorted
    pivot = arr[-1]  # Choose last element as pivot
    smaller, equal, larger = [], [], []
    for num in arr:
        if num < pivot:
            smaller.append(num)
        elif num == pivot:
            equal.append(num)
        else:
            larger.append(num)
    return quicksort(smaller) + equal + quicksort(larger)  # Recursively sort sub-arrays and concatenate
```
### Error Handling:
- The function handles the case where the input array is empty or has only one element, in which case it simply returns the array as it is already sorted.

With reasoning, the model internalizes the algorithm before coding it. This leads to clearer logic, better error handling, and code that’s easier for humans to audit or extend.

Self-Consistency: Multiple Reasoning Paths

For particularly complex problems, instructing the model to generate multiple independent solutions and then select the best one significantly improves reliability. This mimics how senior developers often approach challenging issues.

Without multiple passes:

Write code to detect cycles in a linked list.

Output:

Here is a Python code snippet to detect cycles in a linked list using Floyd's Tortoise and Hare algorithm:
```python
class ListNode:
    def __init__(self, x):
        self.val = x
        self.next = None
def hasCycle(head):
    if not head or not head.next:
        return False
    slow = head
    fast = head.next
    while slow != fast:
        if not fast or not fast.next:
            return False
        slow = slow.next
        fast = fast.next.next
    return True
```
You can use this `hasCycle` function to check if a linked list has a cycle by passing the head of the linked list as an argument. If the linked list has a cycle, the function will return `True`; otherwise, it will return `False`.

With multiple options:

Generate three different approaches to detect cycles in a linked list. For each approach:
Explain the algorithm’s logic
Analyze its time and space complexity
Implement it in code

Then, compare the approaches and recommend which one should be used in a production environment with potential memory constraints.

Output:

### Approach 1: Using a Hash Set
#### Algorithm Logic:
- Traverse the linked list nodes one by one.
- At each node, check if the node is already present in a hash set.
- If the node is already in the hash set, there is a cycle. If not, add the node to the hash set.
- Continue this process until reaching the end of the list.
#### Time Complexity:
- Time complexity is O(n) where n is the number of nodes in the linked list.
#### Space Complexity:
- Space complexity is O(n) for the hash set storing the visited nodes.
#### Implementation:
```python
class ListNode:
    def __init__(self, value):
        self.value = value
        self.next = None
def detect_cycle_hash_set(head: ListNode) -> bool:
    visited = set()
    current = head
    while current:
        if current in visited:
            return True
        visited.add(current)
        current = current.next
    return False
```
### Approach 2: Floyd's Cycle Detection Algorithm (Two Pointers)
#### Algorithm Logic:
- Use two pointers, one slow and one fast.
- Move the slow pointer one step and the fast pointer two steps at a time.
- If there is a cycle, the slow and fast pointers will meet at some point.
#### Time Complexity:
- Time complexity is O(n) where n is the number of nodes in the linked list.
#### Space Complexity:
- Space complexity is O(1) as we are using only two pointers.
#### Implementation:
```python
def detect_cycle_floyd(head: ListNode) -> bool:
    slow = head
    fast = head
    while fast and fast.next:
        slow = slow.next
        fast = fast.next.next
        if slow == fast:
            return True
    return False
```
### Approach 3: Reverse and Compare
#### Algorithm Logic:
- Reverse the linked list.
- Compare the reversed list with the original list.
- If they are the same, there is a cycle.
#### Time Complexity:
- Time complexity is O(n) where n is the number of nodes in the linked list.
#### Space Complexity:
- Space complexity is O(1) as we are not using any extra space.
#### Implementation:
```python
def reverse_list(head: ListNode) -> ListNode:
    prev = None
    current = head
    while current:
        next_node = current.next
        current.next = prev
        prev = current
        current = next_node
    return prev
def detect_cycle_reverse_compare(head: ListNode) -> bool:
    reversed_head = reverse_list(head)
    while head and reversed_head:
        if head == reversed_head:
            return True
        head = head.next
        reversed_head = reversed_head.next
    return False
```
### Recommendation:
- For a production environment with potential memory constraints, Floyd's Cycle Detection Algorithm (Approach 2) should be used.
- It has a low space complexity of O(1) and a time complexity of O(n), making it efficient in terms of memory usage and performance.

By analyzing self-consistency, you shift from accepting the first answer to evaluating multiple valid implementations. This mirrors how experienced developers consider tradeoffs before committing to a solution.

Skeleton Prompting: Fill-in-the-Blank for Structured Control

When you need precise control over the structure of generated code, provide a skeleton that the AI can fill in. This is particularly effective for ensuring adherence to specific architectural patterns or coding standards.

With no skeleton:

Create a React component for a user profile page.

Output:

<script src=”https://gist.github.com/ajtatey/44bb6dcd05eb0bb2ff61bdeac168de09.js”></script>

With a structure:

Complete the following React component skeleton for a user profile page that handles loading states, errors, and displays user information:

const UserProfile = ({ userId }) => {
  // TODO: Add state for user data, loading, and errors
  
  // TODO: Add useEffect to fetch user data
  
  // TODO: Add loading state JSX
  
  // TODO: Add error state JSX
  
  // TODO: Add render method with user information display
  
  // TODO: Add any helper methods needed
}

Use TypeScript types and follow best practices for React hooks.

Output:

<script src=”https://gist.github.com/ajtatey/ba65b79145391f81333b6a0408295f26.js“></script>

The skeleton means the AI no longer has to guess your structure—it’s filling in blanks rather than making architectural decisions. This increases alignment with standards and reduces post-generation cleanup.

Output Schemas & Format Directives: Enforcing Structure

When integration with other systems is crucial, explicitly defining the expected output format ensures compatibility and reduces manual transformation work.

With no specific output:

Generate a function that returns weather data for a given location.

# Output

Sure! Here is an example of a function in Python that retrieves weather data for a given location using the OpenWeatherMap API:
```python
import requests
def get_weather_data(api_key, location):
    url = f"http://api.openweathermap.org/data/2.5/weather?q={location}&appid={api_key}"
    response = requests.get(url)
    
    if response.status_code == 200:
        data = response.json()
        weather = data['weather'][0]['description']
        temp_kelvin = data['main']['temp']
        temp_celsius = round(temp_kelvin - 273.15, 2)
        return f"The weather in {location} is {weather} with a temperature of {temp_celsius}°C."
    else:
        return "Failed to retrieve weather data. Please check the location and try again."
# Replace 'YOUR_API_KEY' with your actual OpenWeatherMap API key
api_key = 'YOUR_API_KEY'
location = 'London'
print(get_weather_data(api_key, location))
```
Make sure to replace `'YOUR_API_KEY'` with your actual OpenWeatherMap API key. This function takes the API key and location as input parameters and returns a string with the weather description and temperature for the specified location.

Output:

Generate a function that returns weather data for a given location.

# Output

Sure! Here is an example of a function in Python that retrieves weather data for a given location using the OpenWeatherMap API:
```python
import requests
def get_weather_data(api_key, location):
    url = f"http://api.openweathermap.org/data/2.5/weather?q={location}&appid={api_key}"
    response = requests.get(url)
    
    if response.status_code == 200:
        data = response.json()
        weather = data['weather'][0]['description']
        temp_kelvin = data['main']['temp']
        temp_celsius = round(temp_kelvin - 273.15, 2)
        return f"The weather in {location} is {weather} with a temperature of {temp_celsius}°C."
    else:
        return "Failed to retrieve weather data. Please check the location and try again."
# Replace 'YOUR_API_KEY' with your actual OpenWeatherMap API key
api_key = 'YOUR_API_KEY'
location = 'London'
print(get_weather_data(api_key, location))
```
Make sure to replace `'YOUR_API_KEY'` with your actual OpenWeatherMap API key. This function takes the API key and location as input parameters and returns a string with the weather description and temperature for the specified location.

With some specific JSON structuring:

Create a function that fetches weather data for a given location. The function should return data in the following JSON structure:

{
  "location": {
    "city": "string",
    "country": "string",
    "coordinates": {
      "lat": "number",
      "long": "number"
    }
  },
  "current": {
    "temperature": "number",
    "humidity": "number",
    "windSpeed": "number",
    "conditions": "string"
  },
  "forecast": [
    {
      "date": "string (YYYY-MM-DD)",
      "highTemp": "number",
      "lowTemp": "number",
      "conditions": "string"
    }
  ]
}

Make sure to include error handling and appropriate documentation.

Output:

<script src=”https://gist.github.com/ajtatey/9b2d00ec46f2de63b99a1a500db473e0.js“></script>

By defining the output structure, you ensure compatibility with consuming systems and reduce the need for brittle regex parsing or post-processing logic. It enforces correctness through specification.

Configuration Parameters: Tuning Prompts Like Runtime Settings

Model settings like temperature, top-p, and max tokens don’t just change style—they reshape the type of output an LLM will return. These are runtime controls that developers should use deliberately. For example, setting temperature: 0 is ideal for deterministic, production-safe code; temperature: 0.7+ enables exploration of novel approaches or variations.

// Example model configuration object
const modelConfigs = {
  production: {
    temperature: 0,
    top_p: 1,
    max_tokens: 1024,
    frequency_penalty: 0,
    presence_penalty: 0
  },
  exploration: {
    temperature: 0.7,
    top_p: 0.9,
    max_tokens: 2048,
    frequency_penalty: 0.3,
    presence_penalty: 0.3
  },
  creative: {
    temperature: 1.2,
    top_p: 0.8,
    max_tokens: 4096,
    frequency_penalty: 0.5,
    presence_penalty: 0.5
  }
};

Temperature fundamentally controls output determinism versus creativity:

TemperatureBehaviorBest For
0.0Completely deterministicProduction code generation, SQL queries, data transformations
0.1 – 0.4Mostly deterministic with slight variationDocumentation generation, explanatory comments
0.5 – 0.7Balanced determinism and creativityDesign patterns, architecture suggestions
0.8 – 1.0Increasingly creative responsesUI/UX ideas, alternative implementations
> 1.0Highly creative, potentially erraticBrainstorming sessions, unconventional approaches

Consider this example of the same prompt with different temperature settings:

# Same prompt, different temperatures
"""
prompt = Write a function to calculate the Fibonacci sequence optimized for performance.;
"""

# Temperature: 0.0
# Result: Consistent, standard memoized solution
def fibonacci(n, memo={}):
    if n in memo:
        return memo[n]
    if n <= 1:
        return n
    memo[n] = fibonacci(n-1, memo) + fibonacci(n-2, memo)
    return memo[n]
# Example usage
n = 10
result = fibonacci(n)
print(f"The {n}th Fibonacci number is: {result}")


# Temperature: 0.7
# Result: Potentially alternative approaches
def fibonacci(n):
    a, b = 0, 1
    for _ in range(n):
        a, b = b, a + b
    return a

# 2nd run at 0.7
def fibonacci(n):
    memo = {}
    def fib_helper(n):
        if n in memo:
            return memo[n]
        if n <= 1:
            return n
        else:
            result = fib_helper(n - 1) + fib_helper(n - 2)
            memo[n] = result
            return result
    return fib_helper(n)
# Example usage
n = 10
print(fibonacci(n))  # Output: 55

By adjusting temperature (or max tokens or top_p), you can identify the right model parameters for your coding style and needs.

Prompt Anatomy: Structure Your Inputs Like Interfaces

Every effective prompt has identifiable sections—persona, task, context, output format, and examples. Breaking prompts down into these components improves clarity and makes them easier to version, document, and reuse. This is the interface layer between you and the model.

A well-structured prompt can be decomposed into distinct components:

  1. Persona: The role or expertise level you want the AI to emulate
  2. Task: The specific action or output you’re requesting
  3. Context: Background information or constraints
  4. Output Structure: The format and organization of the response
  5. Examples: Demonstrations of desired outputs (few-shot learning)

A component-based system allows you to mix and match pre-defined modules rather than crafting these elements from scratch each time.

Component Library Example

Here’s how a component-based prompt system might look in practice:

// Personas
const personas = {
  backendExpert: `You are a senior backend developer with 15+ years of experience 
                  specializing in distributed systems and API design. You prioritize 
                  scalability, security, and maintainability in all your code.`,
  
  securitySpecialist: `You are a cybersecurity expert with deep knowledge of 
                       application security, threat modeling, and secure coding practices. 
                       You always identify potential vulnerabilities in code.`
};

// Output Formats
const formats = {
  json: `Return your response as a valid JSON object without any additional explanation.`,
  
  markdown: `Format your response as markdown with appropriate headings, 
             code blocks with language tags, and bullet points where relevant.`
};

// Task Templates
const tasks = {
  codeReview: `Review the following code for {aspect} issues:
               
               \`\`\`{language}
               {code}
               \`\`\`
               
               Focus particularly on {focus_area}.`,
  
  implementation: `Write {language} code that implements {feature} with the 
                   following requirements:
                   
                   {requirements}`
};

// Composing a complete prompt
function createPrompt(components) {
  return [
    components.persona,
    components.task,
    components.format
  ].join('\n\n');
}

// Usage
const securityReviewPrompt = createPrompt({
  persona: personas.securitySpecialist,
  task: tasks.codeReview
    .replace('{aspect}', 'security')
    .replace('{language}', 'python')
    .replace('{code}', userCode)
    .replace('{focus_area}', 'input validation and SQL injection'),
  format: formats.markdown
});

This component-based approach delivers several advantages:

  1. Consistency: Standardized components ensure uniform outputs across your application
  2. Maintainability: Update a component once to affect all prompts using it
  3. Version Control: Track changes to prompt components like any other code
  4. Collaboration: Teams can share and reuse components across projects
  5. Testing: Validate individual components for reliability
  6. Documentation: Self-documenting prompt architecture

Prompt Linting: Validate Structure Before Execution

Just as developers rely on linters to catch code issues before runtime, prompt engineers need automated quality checks to identify structural problems before execution. Before launching your prompts into production, validating them for clarity, completeness, and consistency can dramatically improve reliability and reduce debugging time.

The Case for Prompt Linting

Prompts are susceptible to several classes of structural issues:

  • Ambiguous instructions: Directions that can be interpreted multiple ways
  • Conflicting constraints: Requirements that contradict each other
  • Missing format directives: Unclear expectations for output structure
  • Forgotten variables: Template placeholders that weren’t replaced
  • Insufficient examples: Few-shot patterns without enough cases
  • Unclear personas: Vague role descriptions for the model

LLM-Powered Self-Linting

The most powerful approach to prompt validation is using the LLM as a linting tool. This meta-use of AI leverages the model’s own understanding of language and reasoning to identify potential issues:

// LLM-based prompt self-linting
async function selfLintPrompt(prompt, model) {
  const metaPrompt = `
    You are a prompt engineering expert tasked with analyzing and improving prompts.
    
    Review the following prompt for:
    1. Clarity of instructions
    2. Potential ambiguities
    3. Structural issues
    4. Missing constraints or format specifications
    5. Consistency of requirements
    
    Provide your analysis in JSON format with the following structure:
    {
      "overallQuality": number, // 1-10 score
      "issues": [
        {
          "type": string, // One of: "ambiguity", "structure", "contradiction", "missing", "other"
          "severity": string, // One of: "error", "warning", "suggestion"
          "description": string,
          "recommendation": string
        }
      ],
      "improvedVersion": string // The revised prompt
    }
    
    Here is the prompt to analyze:
    
    """
    ${prompt}
    """
    
    Provide ONLY the JSON output without any additional text.
  `;
  
  const analysisResult = await model.complete(metaPrompt, { temperature: 0.1 });
  
  try {
    return JSON.parse(analysisResult);
  } catch (e) {
    return {
      error: "Failed to parse LLM output as JSON",
      rawOutput: analysisResult
    };
  }
}

If we gave it this prompt to lint:

Generate a React component that displays user information from an API. Make it look good and add some nice features if possible.

<script src=”https://gist.github.com/ajtatey/5a500d536a6ab5b01c80feec4762cf89.js“></script>

Which would then produce this code:

<script src=”https://gist.github.com/ajtatey/4be3cd7bfc548767d8aa78c213c49438.js“></script>

In this way, we get LLMs to produce better and better prompts, leading to better and better code.

Prompts Are Code

Prompt engineering is becoming a proper developer discipline with patterns, tools, and methodologies just like any other area of coding. You wouldn’t write a function without tests, so why would you deploy a prompt without validation? You version control your code, so shouldn’t you do the same with your prompts? The parallels are everywhere.

What makes this approach powerful is how it leverages existing software development practices. Few-shot examples are basically test cases. Chain-of-thought is like forcing the model to show its work. Skeleton prompting gives you the same control as template patterns in traditional code. And when you apply these techniques consistently, the unpredictability that makes people nervous about AI starts to melt away. You can confidently ship AI-powered features knowing they’ll behave as expected, just like any other component in your system.

Stop treating your prompts like throwaway strings. Build them like software, test them like software, and maintain them like software–and watch your AI interactions become as reliable as the rest of your codebase.


Neon is the serverless Postgres database used by Replit Agent and Create.xyz to provision databases when building apps. It also works like a charm with Cursor and Windsurf via its MCP Server. Sign up for Neon (we have a Free Plan) and start building.