I wasted two months using AI coding tools the wrong way.
I’d type “create a login form” and get mediocre code. I’d ask “fix this bug” and the AI would be confused. I thought the tools weren’t as good as everyone claimed.
Then I learned prompt engineering, and everything changed. Same tools, 10x better results.
The difference between developers who love AI coding assistants and those who hate them isn’t the tool they’re using—it’s how they ask. A well-crafted prompt gets you production-ready code in seconds. A bad prompt wastes time and generates garbage.
After writing thousands of prompts across Cursor AI, GitHub Copilot, ChatGPT, and Claude, I’ve identified patterns that consistently work. This guide will show you the exact formulas I use every day.
Quick Start Summary
What You’ll Learn:
– The anatomy of an effective code prompt
– Specific vs vague: The #1 factor in prompt quality
– Context provision techniques
– Prompt templates for common tasks
– Platform-specific tips (Cursor vs Copilot vs ChatGPT)
– Anti-patterns to avoid
Prerequisites:
– Experience with at least one AI coding tool
– Basic understanding of your programming language
– Willingness to be more specific than you think necessary
Key Insight: The best prompts are specific, contextual, and constrained. Vague prompts = vague code.
The Fundamental Principle of Code Prompts
The Specificity Rule
Bad prompt (vague):
Create a form
Good prompt (specific):
Create a React login form with:
- Email input (validated with regex)
- Password input (min 8 chars, show/hide toggle)
- "Remember me" checkbox
- Submit button (disabled while loading)
- Error message display below form
- Uses Tailwind CSS for styling
- Form validation with react-hook-form
Code language: JavaScript (javascript)
Why Specificity Matters
AI coding models are trained on millions of code examples. When you say “create a form,” the AI has to guess:
– What framework? (React? Vue? Angular? Vanilla JS?)
– What styling? (CSS? Tailwind? Material UI?)
– What validation? (Built-in? Library? None?)
– What fields? (Login? Signup? Contact?)
The AI will make reasonable guesses, but they’re rarely what you actually want.
The more specific you are, the less the AI has to guess.
Anatomy of a Perfect Code Prompt
The 5-Part Framework
Every great code prompt has these five elements:
[1. ACTION] + [2. CONTEXT] + [3. REQUIREMENTS] + [4. CONSTRAINTS] + [5. STYLE]
Code language: CSS (css)
1. ACTION – What to do
Be explicit about the operation:
– “Create” / “Build” / “Generate” (new code)
– “Refactor” / “Rewrite” / “Optimise” (modify existing)
– “Fix” / “Debug” / “Resolve” (fix issues)
– “Explain” / “Document” / “Comment” (add documentation)
– “Test” / “Validate” / “Check” (testing/QA)
2. CONTEXT – What exists
Provide relevant information:
– Current code structure
– File locations
– Existing patterns
– Related components
– Tech stack details
3. REQUIREMENTS – What it must do
Functional specifications:
– Features list
– User interactions
– Data flow
– API endpoints
– Expected behavior
4. CONSTRAINTS – What it can’t do
Limitations and boundaries:
– Performance requirements
– Security considerations
– Compatibility needs
– Don’t break existing code
– Specific libraries to use/avoid
5. STYLE – How it should look
Code style and patterns:
– Follow existing code style
– Use specific design patterns
– Match other files
– Naming conventions
– Comment style
Real Example Using All 5 Parts
[ACTION]
Create a user authentication service
[CONTEXT]
In our Express.js API (src/services/), we use:
- PostgreSQL with Prisma ORM
- JWT tokens for auth
- bcrypt for password hashing
[REQUIREMENTS]
The service should provide:
- register(email, password): Creates user, returns JWT
- login(email, password): Validates credentials, returns JWT
- verifyToken(token): Validates JWT, returns user data
- refreshToken(token): Issues new JWT from valid refresh token
[CONSTRAINTS]
- Passwords must be hashed with bcrypt (10 rounds)
- JWTs expire in 15 minutes, refresh tokens in 7 days
- Validate email format with regex
- Don't expose password hashes in responses
- Handle all errors gracefully (no unhandled rejections)
[STYLE]
- Use async/await (no callbacks)
- Follow the same pattern as src/services/UserService.ts
- Add JSDoc comments for all public methods
- Use TypeScript interfaces for all parameters
Code language: PHP (php)
This prompt will generate near-perfect code on the first try.
Proven Prompt Templates
Template 1: Creating a New Component
Create a [FRAMEWORK] [COMPONENT_TYPE] component for [PURPOSE]:
Features:
- [Feature 1]
- [Feature 2]
- [Feature 3]
Props:
- [prop1]: [type] - [description]
- [prop2]: [type] - [description]
Styling:
- Use [STYLING_LIBRARY]
- Match the design of [REFERENCE_COMPONENT]
Example usage:
<ComponentName prop1="value" prop2={value} />
Code language: HTML, XML (xml)
Real example:
Create a React UserCard component for displaying user profiles:
Features:
- Shows avatar, name, email, and role
- Clickable to navigate to user detail page
- Hover effect with subtle shadow
- Loading state with skeleton UI
Props:
- user: User - user object with id, name, email, avatar, role
- onClick?: () => void - optional click handler
- loading?: boolean - shows skeleton if true
Styling:
- Use Tailwind CSS
- Match the design of PostCard.tsx in same folder
Example usage:
<UserCard user={userData} onClick={() => navigate('/user/123')} />
Code language: JavaScript (javascript)
Template 2: Adding an API Endpoint
Add a [HTTP_METHOD] [ENDPOINT] endpoint in [FILE]:
Purpose: [What this endpoint does]
Request:
- Method: [GET/POST/PUT/DELETE]
- Path: [/api/path/:param]
- Body: [JSON structure]
- Query params: [?param=value]
Response:
- Success (200): [JSON structure]
- Error (4XX): [Error format]
Logic:
1. [Step 1]
2. [Step 2]
3. [Step 3]
Validation:
- [Validation rule 1]
- [Validation rule 2]
Follow the pattern of existing routes in [REFERENCE_FILE]
Code language: CSS (css)
Real example:
Add a POST /api/comments endpoint in src/routes/comments.ts:
Purpose: Create a new comment on a blog post
Request:
- Method: POST
- Path: /api/comments
- Body: { postId: string, content: string }
- Headers: Authorization: Bearer <token>
Response:
- Success (201): { id, postId, userId, content, createdAt }
- Error (400): { error: "Validation failed", details: [...] }
- Error (401): { error: "Unauthorized" }
Logic:
1. Verify JWT token and extract userId
2. Validate postId exists in database
3. Validate content (min 1 char, max 500 chars)
4. Create comment in database
5. Return created comment with user info
Validation:
- content must be 1-500 characters
- postId must be valid UUID
- User must be authenticated
Follow the pattern of existing routes in src/routes/posts.ts
Code language: JavaScript (javascript)
Template 3: Refactoring Code
Refactor [FILE/FUNCTION] to [IMPROVEMENT]:
Current issues:
- [Problem 1]
- [Problem 2]
Target improvements:
- [Improvement 1]
- [Improvement 2]
Requirements:
- Don't change the public API (same inputs/outputs)
- Maintain all existing functionality
- Add [new requirement if any]
Specific changes:
- [Change 1]
- [Change 2]
Real example:
Refactor src/utils/dataFetcher.ts to use async/await instead of callbacks:
Current issues:
- Nested callbacks causing callback hell
- Error handling is inconsistent
- Hard to test with current structure
Target improvements:
- Convert all functions to async/await
- Consistent try/catch error handling
- Return standardized { data, error } objects
Requirements:
- Don't change function names (used throughout app)
- Maintain the same input parameters
- Keep backward compatibility with existing code
Specific changes:
- fetchData(url, callback) → async fetchData(url)
- Add proper TypeScript return types
- Add JSDoc comments
Code language: PHP (php)
Template 4: Debugging/Fixing Issues
Fix [BUG_DESCRIPTION] in [FILE]:
Current behavior:
[What's happening now]
Expected behavior:
[What should happen]
Error message:
[Paste full error]
Relevant code:
[Paste code section]
Context:
[Environment, dependencies, related files]
Real example:
Fix infinite re-render loop in src/components/Dashboard.tsx:
Current behavior:
Component renders continuously, browser freezes, console shows "Maximum update depth exceeded"
Expected behavior:
Component should render once and only re-render when user data changes
Error message:
Warning: Maximum update depth exceeded. This can happen when a component calls setState inside useEffect, but useEffect either doesn't have a dependency array, or one of the dependencies changes on every render.
Relevant code:
useEffect(() => {
setStats(calculateStats(userData));
}, [userData]);
function calculateStats(data) {
return { total: data.length, active: data.filter(u => u.active) };
}
Context:
- React 18.2
- userData is passed as prop from parent
- calculateStats creates new object each time
Code language: PHP (php)
Template 5: Writing Tests
Generate [TEST_TYPE] tests for [FILE/FUNCTION]:
Test coverage needed:
- [Scenario 1]
- [Scenario 2]
- [Edge case 1]
- [Error case 1]
Testing setup:
- Framework: [Jest/Vitest/etc]
- Mocking: [What needs to be mocked]
- Test data: [Describe test fixtures]
Follow the pattern of [EXISTING_TEST_FILE]
Code language: CSS (css)
Real example:
Generate unit tests for src/utils/validateEmail.ts:
Test coverage needed:
- Valid email formats (user@domain.com, user+tag@domain.co.uk)
- Invalid formats (no @, no domain, special chars, spaces)
- Edge cases (empty string, null, undefined, very long emails)
- Returns true for valid, false for invalid
Testing setup:
- Framework: Jest
- No mocking needed (pure function)
- Test data: Array of valid/invalid email examples
Follow the pattern of tests/utils/validatePassword.test.ts
Code language: JavaScript (javascript)
Platform-Specific Prompt Tips
Cursor AI Prompts
Best practices for Cursor (Cmd+K and Composer):
- Always select code first (for Cmd+K)
[Select function] → Cmd+K → "Add error handling" - Mention file locations (for Composer)
In src/components/Auth.tsx, add loading states - Reference other files as examples
Style this the same way as Dashboard.tsx - Use “looking at” for context
Looking at UserService.ts, create a similar PostService.ts - Be explicit about multi-file changes
Update both src/routes/api.ts and src/types/api.ts
GitHub Copilot Prompts
Best practices for Copilot:
- Write comments above where you want code
javascript
// Create async function that fetches user data from API
// Handle loading and error states
// Return { data, loading, error } - Use Copilot Chat slash commands
/explain [for understanding]
/fix [for debugging]
/tests [for test generation] - Select code + open inline chat (Ctrl+I)
[Select code] → Ctrl+I → "Convert to TypeScript" - Use @workspace for project-wide questions
@workspace how is authentication handled in this project?
ChatGPT / Claude Prompts
Best practices for standalone AI chats:
- Provide full context (they can’t see your files)
I'm building a React app with TypeScript and Tailwind.
Here's my current UserProfile component:
[Paste code]
I want to add... - Ask for explanations with code
Explain how this works and provide a refactored version:
[Paste code] - Request step-by-step instructions
Step-by-step guide to add JWT authentication to my Express API - Ask for comparisons
Compare these two approaches and recommend the best one:
Approach A:[Code]
Approach B:[Code]
Advanced Prompting Techniques
Technique 1: Constrained Generation
Force the AI to follow specific rules:
Create a React component following these strict rules:
1. Maximum 50 lines of code
2. No external dependencies beyond React
3. Must be a pure functional component (no hooks)
4. All styling inline (no CSS files)
5. Fully accessible (ARIA labels, keyboard navigation)
Technique 2: Example-Driven Prompts
Show the AI exactly what you want:
I have this function:
function fetchUser(id) {
return fetch(`/api/users/${id}`).then(res => res.json());
}
Create similar functions for:
- fetchPost(id)
- fetchComments(postId)
- fetchUserPosts(userId)
Same structure, different endpoints.
Code language: JavaScript (javascript)
Technique 3: Incremental Refinement
Build complexity gradually:
Round 1: "Create a basic button component"
[Review]
Round 2: "Add variants: primary, secondary, danger"
[Review]
Round 3: "Add loading state with spinner"
[Review]
Round 4: "Add size prop: small, medium, large"
[Final version]
Code language: PHP (php)
Technique 4: Negative Constraints
Tell the AI what NOT to do:
Create a user signup form.
Do NOT:
- Use any external validation libraries
- Add password strength indicator (will add later)
- Include social login buttons
- Add reCAPTCHA (not needed yet)
- Use class components (functional only)
Code language: PHP (php)
This prevents the AI from over-engineering.
Technique 5: Role-Based Prompting
Frame the request from a specific role:
As a senior backend engineer reviewing this code, identify:
- Security vulnerabilities
- Performance bottlenecks
- Error handling issues
- Best practice violations
[Paste code]
Code language: JavaScript (javascript)
Technique 6: Test-Driven Prompting
Start with tests, generate implementation:
Here are the test cases my function needs to pass:
test('adds two numbers', () => {
expect(calculate('2 + 3')).toBe(5);
});
test('handles multiplication', () => {
expect(calculate('4 * 5')).toBe(20);
});
test('respects order of operations', () => {
expect(calculate('2 + 3 * 4')).toBe(14);
});
Create the calculate() function that passes these tests.
Code language: PHP (php)
Common Prompt Anti-Patterns
Anti-Pattern #1: The Vague Ask
Bad:
Make this better
Code language: JavaScript (javascript)
Why it fails: “Better” is subjective. Better how? Performance? Readability? Features?
Fix:
Refactor this to improve readability:
- Extract magic numbers into named constants
- Add comments for complex logic
- Break long function into smaller helpers
Code language: JavaScript (javascript)
Anti-Pattern #2: The Context-Free Question
Bad:
Why doesn't this work?
[Pastes code with no error message]
Why it fails: AI doesn’t know what “work” means or what error you’re seeing.
Fix:
This login function returns undefined instead of a user object.
Code:
[Paste code]
Error in console:
Cannot read property 'id' of undefined
Expected: Should return user object from database
Anti-Pattern #3: The Kitchen Sink
Bad:
Create a complete e-commerce website with user auth, product catalog, shopping cart, payment processing, admin dashboard, email notifications, and analytics
Code language: JavaScript (javascript)
Why it fails: Way too broad. AI will give you a high-level skeleton, not working code.
Fix: Break into 20 smaller prompts, one feature at a time.
Anti-Pattern #4: The Assumed Context
Bad:
Add the user profile feature
Why it fails: AI doesn’t know what “the user profile feature” means in your specific app.
Fix:
Add a user profile page (src/pages/Profile.tsx):
- Shows user's name, email, avatar
- "Edit Profile" button navigates to /profile/edit
- Uses UserCard component from src/components/
- Fetches data from GET /api/users/:id
- Matches styling of Dashboard.tsx
Anti-Pattern #5: The No-Constraints Ask
Bad:
Create a form validation library
Why it fails: AI will create something generic that doesn’t fit your needs.
Fix:
Create a form validation library:
- React hooks-based (useForm, useField)
- Schema validation with Zod
- Built-in async validation support
- TypeScript with full type inference
- < 5KB bundle size
- No dependencies beyond Zod
Code language: JavaScript (javascript)
Debugging Bad Prompts
If you’re not getting good results, ask yourself:
Question 1: Is it specific enough?
Too vague: “Create a dashboard”
Specific: “Create a React dashboard with user stats cards, a recent activity table, and a chart showing signups over time”
Question 2: Did I provide context?
No context: “Fix this bug”
With context: “Fix authentication bug in src/auth.ts – JWT verification fails with ‘invalid signature’ error when token is valid”
Question 3: Did I mention technical requirements?
No tech: “Create an API endpoint”
With tech: “Create a POST endpoint in Express.js with Zod validation and Prisma database access”
Question 4: Did I reference existing patterns?
No reference: “Style this component”
With reference: “Style this component using Tailwind classes matching the design of Header.tsx”
Question 5: Did I set constraints?
No constraints: “Optimise this code”
With constraints: “Optimise this code for performance without changing the public API or adding dependencies”
Measuring Prompt Quality
The First-Try Success Rate
Track how often your prompts work on the first try:
- < 30%: Your prompts need more specificity
- 30-60%: Average (still room for improvement)
- 60-80%: Good prompt engineering
- > 80%: Excellent prompting skills
The Edit Distance
How much do you need to modify AI-generated code:
- Major rewrites: Prompt was too vague
- Minor tweaks: Good prompt, small AI mistakes
- Copy-paste ready: Excellent prompt
Goal: Aim for minor tweaks or less.
Prompt Engineering Checklist
Before submitting a prompt, verify:
- [ ] Specified the exact action (create, refactor, fix, etc.)
- [ ] Provided relevant context (tech stack, file structure)
- [ ] Listed specific requirements (features, behaviour)
- [ ] Set constraints (what NOT to do)
- [ ] Mentioned code style preferences
- [ ] Referenced existing files as examples (if applicable)
- [ ] Included error messages (if debugging)
- [ ] Avoided vague words (“better,” “improve,” “optimise”)
Real-World Before/After Examples
Example 1: Creating a Component
Before (vague):
Create a login form
After (specific):
Create a React login form component (src/components/LoginForm.tsx):
Fields:
- Email input with validation (must be valid email)
- Password input with show/hide toggle (min 8 chars)
UI:
- Submit button (disabled during submission)
- Error message display (red text below form)
- "Forgot password?" link
- "Sign up" link
Behavior:
- On submit: Call loginUser(email, password) API function
- Show loading spinner in button during submission
- Display API error if login fails
- Navigate to /dashboard on success
Styling:
- Use Tailwind CSS
- Match the card style from SignupForm.tsx
- Mobile responsive (stack fields on small screens)
Validation:
- Use react-hook-form for validation
- Show errors below each field
Code language: JavaScript (javascript)
Result: First prompt generated production-ready code.
Example 2: Debugging
Before (context-free):
Fix the error in fetchData
After (full context):
Fix TypeError in src/api/fetchData.ts:
Error message:
TypeError: Cannot read properties of undefined (reading 'map')
at fetchData (fetchData.ts:23)
Current code:
async function fetchData(userId) {
const response = await fetch(`/api/users/${userId}/posts`);
const data = await response.json();
return data.posts.map(post => ({ ...post, userId }));
}
Issue:
When API returns error (404), data structure is { error: "Not found" }, not { posts: [...] }, causing the error.
Expected behavior:
- Handle error responses gracefully
- Return empty array if posts don't exist
- Don't crash on API errors
Code language: PHP (php)
Result: AI immediately identified the issue and provided proper error handling.
Example 3: Refactoring
Before (vague improvement):
Make this code better
Code language: JavaScript (javascript)
After (specific improvements):
Refactor src/components/UserList.tsx for performance:
Current issues:
- Re-renders on every parent update (even when users prop unchanged)
- Sorts users array on every render (expensive for 1000+ users)
- Creates new event handler functions on every render
Target improvements:
- Wrap component in React.memo
- Use useMemo for sorted users array
- Use useCallback for event handlers
- Split large component into UserListItem subcomponent
Requirements:
- Don't change component API (same props)
- Maintain all existing functionality
- Add comments explaining optimizations
Code language: PHP (php)
Result: AI optimised the component with proper React patterns.
Language-Specific Prompt Tips
JavaScript / TypeScript
[Always specify:]
- Use TypeScript or JavaScript
- Module system (ESM, CommonJS)
- Target environment (Node, Browser, both)
- Async pattern (async/await, promises, callbacks)
Example:
"Create a TypeScript function using ESM exports and async/await"
Code language: JavaScript (javascript)
Python
[Always specify:]
- Python version (3.x)
- Type hints (yes/no)
- Docstring style (Google, NumPy, reStructuredText)
- Libraries to use
Example:
"Create Python 3.11 function with type hints and Google-style docstrings"
Code language: JavaScript (javascript)
React
[Always specify:]
- Functional or class component
- Hooks used
- State management (local, Context, Redux)
- Styling approach (CSS, Tailwind, styled-components)
Example:
"Create functional React component using hooks and Tailwind CSS"
Code language: CSS (css)
Backend APIs
[Always specify:]
- Framework (Express, FastAPI, etc.)
- Database (PostgreSQL, MongoDB, etc.)
- ORM/ODM (Prisma, Mongoose, etc.)
- Auth method (JWT, sessions, etc.)
Example:
"Create Express.js endpoint using Prisma for PostgreSQL with JWT auth"
Code language: JavaScript (javascript)
Next Steps
You now understand how to write effective prompts for AI coding tools. To improve further:
- Practice daily – Rewrite your prompts before submitting
- Analyse failures – When AI code is wrong, identify what your prompt missed
- Build a personal library – Save your best prompts for reuse
- Read related guides:
- How to Use Cursor AI – Cursor-specific techniques
- How to Use GitHub Copilot Chat – Copilot prompting
- Cursor Composer Mode Guide – Multi-file prompting
FAQ
Do I need to write long prompts every time?
No. For simple tasks, short prompts work fine: “Add error handling” or “Convert to async/await.” Save detailed prompts for complex features, new components, or when simple prompts fail.
How do I know if my prompt is too long?
If you’re writing more than 300 words, consider breaking into multiple prompts. Exception: Complex features genuinely need detailed specs. Use the 5-part framework to stay organized.
Can I reuse prompts across projects?
Yes! Build a personal prompt library. Replace project-specific details (file paths, component names) but keep the structure. Great prompts are templates you adapt.
What if the AI still gets it wrong?
Iterate. Don’t rewrite from scratch—refine: “Keep the same structure but change X to Y” or “Fix the error on line 23 where Z happens.” AI understands incremental feedback better than starting over.
Should I use different prompting styles for different AI tools?
Yes, slightly. Cursor benefits from file path mentions. Copilot works well with comment-style prompts. ChatGPT needs more context since it can’t see your code. But the core principles (specificity, context, constraints) apply everywhere.
How do I prompt for security-critical code?
Always add explicit security constraints:
"Sanitize all user inputs to prevent XSS"
"Use parameterized queries to prevent SQL injection"
"Validate and escape all outputs"
"Don't log sensitive data"
Code language: JSON / JSON with Comments (json)
Then manually review the generated code. Never trust AI blindly for auth, payments, or data validation.
Can prompting replace understanding code?
No. Prompt engineering amplifies your skills; it doesn’t replace them. You need to understand what you’re asking for, evaluate if the generated code is correct, and debug issues. AI is a powerful assistant, not a replacement for knowledge.
Related Articles:
– How to Use Cursor AI: Complete Beginner’s Guide
– Cursor Composer Mode: Advanced Multi-File Editing
– How to Use GitHub Copilot Chat
Last updated: January 2025