Get in touch.

Overview

A new generation of AI tools is giving nontraditional techies the chance to build sophisticated applications. Software engineering is not their day job, but they are effective nonetheless. These are AI citizen developers.

If you want to make an app with Claude Code, Replit, GPT-4, or similar AI coding assistants, this will help you think critically about building AI-powered software for internal use. In some cases, you’ll learn when to consult an AI or software engineering expert.

Why I’m writing this

It’s been a few years since I originally wrote about the preventable pitfalls of citizen development. Back then, I focused on the challenges of building applications with traditional no-code platforms like Airtable, Zapier, and AppSheet. These platforms are great for simple tasks but can lead to complex, unmanageable systems if not used carefully. But as any landscape changes over time by nature, the lithosphere of no-code tools has evolved into a new generation of AI-powered coding assistants.

This AI tooling landscape is evolving rapidly. These tools promise to bridge technology gaps where budgets and specialized technical knowledge are limited, often with even more ambitious promises than traditional no-code platforms. They claim near-magical abilities to understand and implement your requirements with minimal technical input.

AI coding tools have unique challenges compared to traditional low-code solutions. While they offer incredible flexibility, they also introduce new classes of risks and technical debt. Professional software engineers can find it difficult to implement simple scenarios in an AI-assisted environment because we recognize gaps in reasoning, security concerns, and maintenance issues that the platform might not prevent. Most of these gaps wouldn’t even occur to someone without the proper training.

To refresh this theme, I bring you Part 2!

For foundational app development concepts, see my previous article on citizen development.

Reliability with AI: New Challenges

Explicit Error Handling for AI-Generated Code

Unlike traditional no-code tools where error handling is often built in, AI coding assistants like Claude Code or GPT might not automatically add robust error checking unless you explicitly ask for it. You’ll need to prompt the AI to include error handling code, logging mechanisms, and graceful recovery options. Something as simple as “Please add proper error handling to this function” can dramatically improve your application’s reliability.

# Before
def process_payment(amount, account):
    api.send_payment(amount, account)  # What if this fails?
    
# After (with explicit prompt for error handling)
def process_payment(amount, account):
    try:
        response = api.send_payment(amount, account)
        log.info(f"Payment of {amount} sent to {account}")
        return {"success": True, "transaction_id": response.id}
    except ConnectionError:
        log.error(f"Connection failed while sending {amount} to {account}")
        return {"success": False, "error": "connection_failed", "retry": True}
    except APIError as e:
        log.error(f"API error: {e.message} when sending {amount} to {account}")
        return {"success": False, "error": e.code, "retry": e.is_transient}

⚠️ Warning: AI code generation tools will happily create infinite retry loops if not properly instructed. Be sure to add backoff mechanisms and maximum retry limits. Otherwise, when your back-end service is already busy, your automated retries will make the problem worse. You might need to manually add timer logic between attempts if the AI doesn’t include it.

I think this code is fine? (It’s not.)

Another critical difference with AI-generated apps is the “looks right but actually wrong” problem. Traditional no-code platforms restrict what you can build to only what works on their platform. AI systems will confidently write code that looks correct but may contain subtle logical errors or security issues.

For example, an AI might generate:

# Looks right but has a subtle bug
def is_valid_account(account_num):
    # Validating account by checking length and starting digit
    return len(account_num) == 10 and account_num[0] == "A"

But this code will crash if account_num is an integer rather than a string, or if the account_num is empty. These edge cases might seem obvious to experienced developers but can be missed by AI systems that focus on the “happy path.”

You can mitigate by encouraging the AI agent to either implement code for unit tests that you’ve already written, or encourage it to achieve a level of code coverage as a matter of course.

Picking Your Poison

The AI revolution has given us two types of “AI” to consider: the AI that helps build your app (like Claude Code) and the AI that you might embed within your app to make decisions. Both require careful human oversight.

When using AI code generators, think of them as enthusiastic junior developers who need code review. Their code will generally work, but may miss edge cases or security considerations. Always review what the AI generates, especially for:

  1. Error handling (as mentioned above)
  2. Input validation and sanitization
  3. Security best practices
  4. Logical correctness in business rules

For AI components within your app (like using an LLM to categorize customer inquiries), you need a different kind of human oversight. Consider this example: you build a payment classification tool that uses AI to determine if a transaction is suspicious:

# This looks smart but could be dangerous without human review
def process_transaction(payment):
    risk_score = ai_model.analyze_risk(payment)
    if risk_score > 0.7:
        automatically_block_transaction(payment)  # Dangerous!
    else:
        process_normally(payment)
        
# A safer approach
def process_transaction(payment):
    risk_score = ai_model.analyze_risk(payment)
    if risk_score > 0.9:  # Only the highest risk
        flag_for_human_review(payment)
    elif risk_score > 0.7:  # Medium risk
        add_additional_verification_step(payment)
    else:
        process_normally(payment)

The most robust AI applications combine algorithmic efficiency with human judgment. Your payment system might use AI to flag unusual transactions, but a human approver makes the final call.

Prompt Engineering as the New Programming

Unlike traditional development where syntax is rigid, AI-assisted development relies heavily on how well you communicate with the AI. This creates a new discipline: prompt engineering.

Writing Clear Technical Prompts

When working with AI coding assistants, clarity trumps brevity. Compare these two prompts:

# Vague prompt
"Make a function to handle payments"

# Clear, specific prompt
"Create a Python function called process_payment that accepts two parameters:
1. amount (a positive float representing dollars)
2. account_id (a string following our XX-YYYY-ZZ format)

The function should:
- Validate both inputs
- Call our payment API at payment_api.send_transaction()
- Include appropriate error handling
- Return a dictionary with success status and transaction ID
- Log all operations using our standard logger"

The second prompt is more likely to produce usable code that meets your requirements and includes proper validation.

Managing Context Windows

Large Language Models (LLMs) have finite “memory” called context windows. When you’re building complex applications, you’ll quickly hit these limits. Strategies for managing context include:

  1. Break down problems: Instead of asking for an entire application at once, request individual components.

  2. Use incremental development: Build your code in layers, asking the AI to explain, then add features to the basic structure.

  3. Context refreshing: If the AI seems to be forgetting earlier parts of your conversation, remind it with summaries like “So far we’ve created X, Y, and now we need to add Z.”

  4. Structured development: Use a clear pattern like “First, let’s design the data models, then the API endpoints, then the business logic.”

Dealing with Hallucinations

AI systems sometimes confidently suggest libraries, functions, or approaches that don’t exist or wouldn’t work. This is especially common with:

  1. Platform-specific features: The AI might suggest Azure functions that don’t exist or have been deprecated.

  2. Library usage: It might invent functions from popular libraries or use them incorrectly.

  3. API interactions: It might assume APIs work in ways they don’t.

Always verify unfamiliar libraries or functions suggested by the AI before implementing them. When working with specific platforms or APIs, have their documentation open in another window to cross-reference.

Data Management and Privacy with AI

Data Exposure Risks

When using AI coding assistants, be extremely cautious about what data you share in prompts. The AI service provider may store your conversations to improve their models, potentially exposing:

  • Internal business logic
  • Security protocols
  • Personal identifiable information (PII)
  • Proprietary algorithms
  • Authentication credentials

Never paste real usernames, passwords, API keys, or sensitive data into AI coding interfaces. Always use placeholder values and sanitize your examples.

Handling Training Data for Embedded AI

If your application includes AI components that you train yourself (like a custom classifier), you need to consider:

  1. Data quality: Biased or incomplete training data will lead to biased or unreliable AI components.

  2. Data storage: Training data often contains sensitive information that needs proper security.

  3. Retraining strategies: AI models degrade over time as real-world conditions change. Plan for regular retraining.

  4. Data rights: Ensure you have appropriate rights to use data for training AI models.

For simple applications, consider using pre-built AI services rather than training your own models when possible.

Further Reading

Prompt Engineering Guide LangChain Documentation AI Safety and Security SOLID Principles Awesome Falsehoods Event-driven architecture

Get the advice you need now.

Reach out to Teleos