Skip to main content
In this lesson you’ll learn how to write development prompts that produce predictable, secure, and maintainable code from an AI assistant. Clear, specific prompts reduce incorrect assumptions, unnecessary back-and-forth, and security oversights.
A presentation slide reading "Writing effective development prompts" on a light background with a dark curved panel on the right. The word "Demo" appears prominently on the dark panel.

Why vague prompts fail

When a prompt is vague, the assistant fills gaps with assumptions. Sometimes those guesses match your intent; often they do not. The minimal example below shows how ambiguity quickly leads to unintended behavior.
* Welcome to the assistant!

cwd: /Users/jeremy/Repos/Claude Code Course/Express-login-demo

> Make a login screen
Because the project folder contains “Express-login-demo”, the assistant assumes an Express-based solution. It may begin exploring the repository and proposing an Express implementation — fine if that’s your intent, but problematic if you expected a different stack or a different authentication flow. The assistant might try discovery commands like:
find /Users/jeremy/Repos/Claude\ Code\ Course/Express-login-demo -type f -name "*.js" -o -name "*.json" -o -name "*.html" -o -name "*.css" | head -20
If the repository is empty, this leads to unnecessary exploration and follow-up questions, slowing development. Vague prompts also cause missing concerns (validation, error handling, security requirements) because the model must guess which constraints matter to you.

A better prompt — be explicit about intent and constraints

A good prompt explicitly states the stack, the endpoint, validation rules, authentication expectations, and desired responses. This reduces ambiguity and lets the assistant produce runnable code with fewer clarifications. Example improved prompt:
Create a POST /api/auth/login endpoint using Express.js with the following requirements:

Input:
- email (string, required, must be a valid email)
- password (string, required, min 8 characters)

Process:
- Validate input using express-validator
- Check if user exists in PostgreSQL database
- Compare password using bcrypt
- Generate JWT token with 24h expiration

Response:
- Success: { token, user: { id, email, name } }
- Failure: Appropriate error message and status code

Include error handling for database failures and validation errors.
Because this prompt specifies technology, validation, security, and responses, the assistant can generate a complete implementation with far fewer clarifying questions.

Example project changes the assistant might create

Below are concise, corrected snippets that implement the requirements above. These files illustrate a minimal but production-minded Express + PostgreSQL + JWT authentication flow. package.json (created by initialization)
{
  "name": "express-login-demo",
  "version": "1.0.0",
  "description": "Express.js login demo with JWT authentication",
  "main": "server.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "test": "echo \"Error: no test specified\" && exit 1"
  },
  "keywords": ["express", "login", "jwt", "authentication"],
  "dependencies": {
    "bcrypt": "^5.0.1",
    "express": "^4.18.2",
    "express-validator": "^6.14.3",
    "jsonwebtoken": "^9.0.0",
    "pg": "^8.11.0",
    "dotenv": "^16.0.0"
  }
}
.env (example)
PORT=3000
JWT_SECRET=your_jwt_secret_key_here
JWT_EXPIRES_IN=24h
# Database configuration
DB_HOST=localhost
DB_PORT=5432
DB_NAME=express_login_demo
DB_USER=your_db_user
DB_PASSWORD=your_db_password
Create directories for configuration and routes:
mkdir -p config routes
config/database.js (Postgres connection using pg)
const { Pool } = require('pg');

const pool = new Pool({
  host: process.env.DB_HOST,
  port: Number(process.env.DB_PORT || 5432),
  database: process.env.DB_NAME,
  user: process.env.DB_USER,
  password: process.env.DB_PASSWORD,
});

pool.on('connect', () => {
  console.log('Connected to PostgreSQL database');
});

pool.on('error', (err) => {
  console.error('Unexpected error on idle client', err);
  process.exit(-1);
});

module.exports = pool;
schema.sql (table for users)
-- Connect to database: \c express_login_demo
CREATE TABLE IF NOT EXISTS users (
  id SERIAL PRIMARY KEY,
  email VARCHAR(255) UNIQUE NOT NULL,
  password_hash VARCHAR(255) NOT NULL,
  name VARCHAR(255),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
routes/auth.js (POST /api/auth/login)
const express = require('express');
const { body, validationResult } = require('express-validator');
const bcrypt = require('bcrypt');
const jwt = require('jsonwebtoken');
const pool = require('../config/database');

const router = express.Router();

router.post(
  '/login',
  [
    body('email').isEmail().withMessage('Valid email is required'),
    body('password').isLength({ min: 8 }).withMessage('Password must be at least 8 characters'),
  ],
  async (req, res) => {
    const errors = validationResult(req);
    if (!errors.isEmpty()) {
      return res.status(400).json({ errors: errors.array() });
    }

    const { email, password } = req.body;

    try {
      const { rows } = await pool.query('SELECT id, email, name, password_hash FROM users WHERE email = $1', [email]);

      if (rows.length === 0) {
        return res.status(401).json({ message: 'Invalid credentials' });
      }

      const user = rows[0];

      const passwordMatch = await bcrypt.compare(password, user.password_hash);
      if (!passwordMatch) {
        return res.status(401).json({ message: 'Invalid credentials' });
      }

      const token = jwt.sign(
        { id: user.id, email: user.email },
        process.env.JWT_SECRET,
        { expiresIn: process.env.JWT_EXPIRES_IN || '24h' }
      );

      return res.json({
        token,
        user: { id: user.id, email: user.email, name: user.name },
      });
    } catch (err) {
      console.error('Database error:', err);
      return res.status(503).json({ message: 'Database unavailable' });
    }
  }
);

module.exports = router;
server.js (main app wiring)
const express = require('express');
const dotenv = require('dotenv');
const authRoutes = require('./routes/auth');

dotenv.config();

const app = express();
app.use(express.json());

app.use('/api/auth', authRoutes);

// Generic error handler
app.use((err, req, res, next) => {
  console.error(err.stack);
  res.status(500).json({ message: 'Internal server error' });
});

const PORT = process.env.PORT || 3000;
app.listen(PORT, () => {
  console.log(`Server listening on port ${PORT}`);
});
Status-code guidance (common HTTP status codes used here)
Status CodeUse Case
400Validation errors (bad input)
401Invalid credentials / unauthorized
503Database connection or service unavailable
500Generic server error
To use the endpoint: set up your PostgreSQL database, configure credentials in .env, run schema.sql to create the users table, install dependencies (npm install), and start the server (npm start or npm run dev).

Prompt-writing formula: Context → Action → Details → Examples

Use a concise formula to craft prompts that minimize follow-up:
  • Context: Describe the environment or codebase (stack, existing endpoints, naming conventions, constraints).
  • Action: Exactly what you want done (for example, “Create POST /api/auth/login endpoint”).
  • Details: Validation rules, libraries to use, security requirements, response shapes, and error handling.
  • Examples: Provide sample request/response payloads or short output examples to set expectations.
Tip: Put the most important constraints (stack, required libraries, validation rules, and response format) at the start of the prompt. Spending a minute to write a clear, complete prompt often saves 10+ minutes of revision.

The three C’s of good prompts

  • Clear: Avoid ambiguity and name the exact behavior you expect.
  • Complete: Include all requirements and constraints up front.
  • Contextual: Provide relevant background—existing patterns, environment, or architecture decisions.
When your prompt is clear, complete, and contextual and includes a short example of expected output, the assistant will produce focused, usable code with minimal rework.

Summary

  • Vague prompts invite incorrect assumptions—be explicit about stack, validation, security, and response formats.
  • Use the Context → Action → Details → Examples structure to make prompts actionable and reproducible.
  • Follow the three C’s: Clear, Complete, Contextual.
  • Small time invested in writing a good prompt yields faster, more secure, and more maintainable results.
You can also consult these guides for deeper prompt-engineering and best practices:

Watch Video