
Prompt Engineering for Developers: Write Better Prompts, Get Better Code
Asking AI to 'make a login page' gives garbage. Structured prompts with context, constraints, and examples produce production-ready code.

Asking AI to 'make a login page' gives garbage. Structured prompts with context, constraints, and examples produce production-ready code.
Both are children of Transformer, so why the difference? Using 'Fill-in-the-blank' vs 'Write-next-word' analogies to explain BERT vs GPT. Practical guide based on trial and error.

ChatGPT answers questions. AI Agents plan, use tools, and complete tasks autonomously. Understanding this difference changes how you build with AI.

Tired of ESLint and Prettier config conflicts? Biome combines linting and formatting in one blazing-fast Rust-based tool.

I actually used all three AI coding tools for real projects. Here's an honest comparison of Copilot, Claude Code, and Cursor.

Until last year, I treated AI like a search engine. I'd type "make me a React login page" expecting something useful. The result? Class components drenched in inline styles, looking like it came straight from 2018. Completely unusable.
The problem wasn't the AI. It was my prompt. AI isn't a restaurant chef. If you order "something tasty," even a chef would be confused. You need to look at the menu, specify ingredients, explain the cooking method to get the dish you want. AI works the same way.
When I started structuring my prompts, the output completely changed. Instead of "make a login page," I gave it role, context, constraints, and examples. Suddenly I got production-ready code. That was prompt engineering. Write prompts like you write code, and AI delivers exactly what you want.
As a developer, I read API docs, match function signatures, specify types. But with AI, I thought I could be vague and it would understand. Big mistake. LLMs are probability-based models. Ambiguous input produces average output. Average code is useless.
I realized prompts should be designed like functions. Clear inputs (context), processing (role and task), outputs (format and constraints). When you debug prompts iteratively, results get increasingly accurate.
Ultimately, prompt engineering isn't learning a new programming language. It's a communication skill for conveying exactly what you want. And developers already have this mindset. Writing code reviews, PR descriptions, issue tickets—we already communicate in structured ways. Prompts are no different.
When I first used AI, my prompts looked like this.
Bad example:Make a login page with React
The result? A 300-line class component with no state management, sloppy validation, zero accessibility considerations. I spent more time fixing it than I would've building from scratch.
I changed the prompt to this.
Good example:Role: You are a senior frontend developer working with Next.js 13 App Router and TypeScript.
Context:
- Project: Authentication system for a SaaS dashboard
- Stack: Next.js 13 App Router, TypeScript, Tailwind CSS, React Hook Form, Zod
- Authentication: Supabase Auth
- Design: shadcn/ui components
Task:
Write a login page component.
Constraints:
- Separate Server Components and Client Components
- Use Zod schema for form validation
- Include error handling (network errors, invalid credentials)
- Handle loading states
- Only Tailwind CSS, no inline styles
- Accessibility with ARIA labels
Example code style:
typescript
const LoginForm = () => {
const form = useForm<LoginFormData>({
resolver: zodResolver(loginSchema),
});
// ...
}
Same AI, same model, but the output was completely different. I got production-ready code with TypeScript type definitions, Zod validation schemas, error boundaries, and loading states.
The difference is clear. The first says "make something." The second says "in this environment, with these constraints, in this style, make this." AI isn't magic. It's a junior developer. Give precise requirements, get precise results.
LLMs only work within their context window. For humans, that's short-term memory. GPT-4 has 128k tokens, Claude has 200k. With Korean consuming more tokens, the actual capacity is less.
Understanding this changed how I write prompts. Remove irrelevant information, keep only essentials. Don't paste 1000 lines of code. Just the problematic function and surrounding type definitions.
// Bad: Paste entire file (500 lines)
Find the bug in this file
[entire file code]
// Good: Extract essentials
This function has a race condition.
Related code:
- fetchUser function (async)
- updateUserCache function
- Call location: UserProfile component useEffect
[only relevant 50 lines]
Expected behavior: During concurrent requests, only apply latest response
Current problem: Earlier request arriving later overwrites data
The context window is like a refrigerator. Stuff everything in and it's hard to find anything. Organize with only necessary ingredients, and the chef (AI) finds and uses them quickly.
After months of trial and error, I settled on this template. The more complex the task, the more strictly I follow this structure.
Tell the AI "this is who you are" first. This sets the tone and direction for the entire response.
Role: You are a 10-year backend engineer specializing in Node.js and PostgreSQL.
Giving a role makes AI think from that perspective. Say "junior developer mentor" and explanations become detailed. Say "code reviewer" and it becomes critically analytical.
Provide current situation, tech stack, project background. Without this, AI gives only generic answers.
Context:
- Project: Real-time chat app
- Stack: Next.js 14, Supabase Realtime, TypeScript
- Current problem: Message delays when concurrent users exceed 100
- Database: PostgreSQL, messages table has no indexes
With this information, AI gives specific solutions considering Supabase characteristics instead of generic advice like "add indexes."
Specify exactly what you want. Vague requests get vague responses.
Task:
Write a migration SQL to improve query performance on the messages table.
"Optimize performance" versus "write migration SQL" produces executable code instead of abstract advice.
Clarify what not to do, rules to follow. Without constraints, AI does whatever.
Constraints:
- Add indexes without losing existing data
- Use CONCURRENTLY option (can't shut down production)
- Composite index on created_at, room_id columns
- Maintain foreign key constraints
Especially important to specify "don't do X." Like "don't recreate table," "don't change existing API."
Show desired output format and code style with examples. This is where few-shot learning kicks in.
Example format:
-- Migration: add_messages_index
-- Date: 2026-01-13
BEGIN;
CREATE INDEX CONCURRENTLY idx_messages_room_created
ON messages(room_id, created_at DESC);
COMMIT;
AI is strong at pattern matching. Give examples and it outputs in exactly that format.
Zero-shot is "do this" with no context. Few-shot is "do it like this, follow the examples." For code generation, few-shot is far more powerful.
Zero-shot (weak):Convert this function to TypeScript
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
}
Result? Full of any types.
Convert to TypeScript following this pattern.
Example 1:
// Before
function getPost(id) {
return fetch(`/api/posts/${id}`).then(r => r.json())
}
// After
type Post = {
id: string;
title: string;
content: string;
};
async function getPost(id: string): Promise<Post> {
const response = await fetch(`/api/posts/${id}`);
if (!response.ok) throw new Error('Failed to fetch post');
return response.json();
}
Now convert using this pattern:
function getUser(id) {
return fetch(`/api/users/${id}`).then(r => r.json())
}
Result? Perfect code with type definitions, error handling, async/await.
Few-shot is like teaching a style guide. Give 2-3 examples, and AI grasps the pattern and produces consistent output.
Throw complex problems at AI and it skips intermediate steps, giving wrong answers. Chain-of-thought prompting asks "show me your thinking process."
Regular prompt:Calculate time complexity of this algorithm
[code]
Answer: "It's O(n^2)." (Wrong)
Chain-of-thought prompt:Analyze the time complexity of this algorithm step-by-step.
Step 1: Determine how many times each loop executes
Step 2: Analyze nested loop relationships
Step 3: Find the dominant term
Step 4: Express in Big-O notation
[code]
Answer: Step-by-step analysis leading to correct O(n log n).
Also useful for complex debugging.
Analyze this bug's cause in the following order:
1. Interpret error message
2. Identify location from stack trace
3. Infer input values to that code
4. Compare expected vs actual behavior
5. Hypothesize root cause
6. Propose fix
[error log and code]
Forcing AI to show its reasoning improves accuracy. Like rubber duck debugging, AI validates its own logic.
When using the API, setting a system prompt maintains consistent behavior across all conversations. Not possible in ChatGPT or Claude UI, but available in APIs or Custom GPTs.
const systemPrompt = `
You are a TypeScript/React code reviewer.
Rules:
- All code must meet TypeScript strict mode standards
- React: only functional components and hooks
- Prefer Tailwind CSS, forbid inline styles
- Always flag performance issues
- Flag accessibility problems (ARIA, keyboard navigation)
Output format:
1. Positive feedback (what's good)
2. Needs improvement (in priority order)
3. Corrected code example
`;
const response = await openai.chat.completions.create({
model: "gpt-4",
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: "Review this component\n[code]" }
]
});
System prompts define AI's "personality." Set once, no need to repeat every time. Works like a team's shared code style guide.
Some patterns I use almost daily. Helpful to save them as templates.
Give AI a specific expert role.
You are a performance optimization expert.
Find unnecessary re-renders in the following React component.
Specify exact output format. JSON, Markdown, code only, etc.
Output only in this JSON format. No explanations, just JSON.
{
"issue": "problem description",
"solution": "fix method",
"code": "corrected code"
}
Break complex tasks into steps.
Refactor in the following steps:
Step 1: Remove duplicate code
Step 2: Separate functions (single responsibility)
Step 3: Strengthen type safety
Step 4: Write tests
Explain changes and reasoning for each step.
Specify what not to do first.
Constraints (absolutely must not violate):
- Don't change existing API endpoints
- Don't change database schema
- Don't add external libraries
Improve performance within these constraints.
Prompts for general users differ from developer prompts. Tips specialized for code generation.
// Bad
Make a React component
// Good
Make it with Next.js 13 App Router, TypeScript, Server Component
Even "React" varies completely by version, framework, rendering method.
Current project structure:
src/
app/
(auth)/
login/
components/
ui/ (shadcn)
lib/
supabase.ts
Add signup page matching this structure
Showing existing patterns produces consistent code.
Debug this error:
Error message:
[full error log with stack trace]
Related code:
[code where error occurs]
Environment:
- Node.js 18.17
- Next.js 14.0.3
- Only happens locally, production is fine
Never summarize error logs. Paste everything for accurate diagnosis.
Our team code style:
// API calls
export async function getUser(id: string) {
const supabase = createClient();
const { data, error } = await supabase
.from('users')
.select('*')
.eq('id', id)
.single();
if (error) throw new Error(error.message);
return data;
}
Create getPosts function using this pattern
Give team conventions as examples, and code review comments decrease.
// Bad
"Optimize performance"
// Good
"Use useMemo and useCallback in React component to eliminate unnecessary re-renders"
"Optimization" means a million things. Be specific.
Paste 5000 lines and say "find the bug," and AI gets lost. Extract essentials only.
"Use TypeScript but any type is fine"
"Performance is important but prioritize readability"
Prioritize clearly. Like "performance critical, readability secondary."
// Bad
"What's this error?"
[error message only]
// Good
"Error occurring when executing Server Action in Next.js 14 App Router.
Environment: Node 18, TypeScript 5.3
Tried: added 'use server', verified async function
[error message + code]"
AI isn't a mind reader. It needs background information.
First prompts are never perfect. Like debugging code, prompts need iterative improvement.
Make a React table component
Result: Basic table tag.
Use TypeScript and Tailwind CSS
Include sorting functionality
Need pagination
Result: Better but design is off.
Style like shadcn/ui Table component
Structure like this:
<Table>
<TableHeader>
<TableRow>
<TableHead>Name</TableHead>
</TableRow>
</TableHeader>
<TableBody>
...
</TableBody>
</Table>
Result: Nearly perfect.
- Add sort arrow icons
- Handle empty state
- Loading skeleton
Result: Production ready.
This process resembles TDD. Start small, incrementally add requirements, verify results while iterating. Don't try to write perfect prompts in one shot. Rapid iteration is more efficient.
Learning prompt engineering taught me this isn't a new skill. It's an extension of what developers already do.
Designing functions: clarify inputs, outputs, constraints. Documenting APIs: specify usage examples, error cases. Writing PRs: include context, changes, test methods.
Prompts work the same way. Think of AI as a teammate. Speak vaguely, get vague results. Communicate concretely and structurally, get what you want.
"Make a login page" is as useless as "do something for me." Give role, context, constraints, examples, and AI creates exactly the code you want.
Prompts are ultimately interfaces. Like well-designed APIs, well-designed prompts are predictable and reusable. And this skill will become increasingly important. In an era of working with AI, prompts will become assets as valuable as code itself.