Providing Context & Constraints for AI Code Generation

# Providing Context and Constraints: The Foundation of Effective AI-Assisted Coding You've probably experienced this: you ask an AI coding assistant to "create a login form," and it generates something that looks nothing like what you need. The styling is off, it uses a different framework, and the validation logic doesn't match your app's requirements. The problem? You didn't provide enough context. Providing context and constraints is the single most important skill in vibe coding. It's the difference between getting code that requires complete rewriting and getting code that's 80% production-ready. In this lesson, you'll learn how to frame your prompts so AI understands not just *what* you want, but *why* and *how* it should fit into your project. ## Why Context and Constraints Matter AI coding assistants are powerful, but they're not mind readers. They work with probabilities based on patterns they've learned. Without context, they make assumptions—often wrong ones—about: - What framework or library you're using - Your coding style and conventions - The existing structure of your codebase - Performance requirements - Security constraints - User experience expectations When you provide context and constraints, you're narrowing the possibility space. You're telling the AI: "Here's the sandbox you're playing in, and here are the rules." This dramatically improves the quality and relevance of generated code. ## The Four Pillars of Context Every effective prompt should include these four types of context: ### 1. Technical Environment Specify the technologies, frameworks, and versions you're working with. **Poor prompt:** ``` Create a button component ``` **Better prompt:** ``` Create a React button component using TypeScript and Tailwind CSS for our Next.js 14 app. Use the app router conventions. ``` The second prompt gives the AI clear technical boundaries. It knows to use React hooks, TypeScript types, Tailwind classes, and Next.js-specific patterns. **Example output difference:** ```typescript // With context - production-ready import { ButtonHTMLAttributes, FC } from 'react'; interface ButtonProps extends ButtonHTMLAttributes { variant?: 'primary' | 'secondary' | 'danger'; isLoading?: boolean; } export const Button: FC = ({ variant = 'primary', isLoading = false, children, className = '', disabled, ...props }) => { const baseStyles = 'px-4 py-2 rounded-lg font-medium transition-colors'; const variantStyles = { primary: 'bg-blue-600 hover:bg-blue-700 text-white', secondary: 'bg-gray-200 hover:bg-gray-300 text-gray-900', danger: 'bg-red-600 hover:bg-red-700 text-white' }; return ( ); }; ``` Without context, you might get a plain HTML button or one using a different framework entirely. ### 2. Business Logic and Requirements Explain what the code needs to accomplish and any business rules it must follow. **Poor prompt:** ``` Create a function to calculate discounts ``` **Better prompt:** ``` Create a TypeScript function to calculate order discounts with these rules: - 10% off orders over $100 - 15% off orders over $250 - 20% off for loyalty members (any order size) - Discounts don't stack - apply the highest applicable discount - Return both the discount amount and final price ``` **Example implementation:** ```typescript interface DiscountResult { originalPrice: number; discountPercent: number; discountAmount: number; finalPrice: number; } function calculateDiscount( orderTotal: number, isLoyaltyMember: boolean = false ): DiscountResult { let discountPercent = 0; // Determine highest applicable discount if (isLoyaltyMember) { discountPercent = 20; } else if (orderTotal > 250) { discountPercent = 15; } else if (orderTotal > 100) { discountPercent = 10; } const discountAmount = orderTotal * (discountPercent / 100); const finalPrice = orderTotal - discountAmount; return { originalPrice: orderTotal, discountPercent, discountAmount, finalPrice }; } // Example usage: console.log(calculateDiscount(150, false)); // { originalPrice: 150, discountPercent: 10, discountAmount: 15, finalPrice: 135 } console.log(calculateDiscount(80, true)); // { originalPrice: 80, discountPercent: 20, discountAmount: 16, finalPrice: 64 } ``` The clear business rules ensure the AI implements the exact logic you need, not a generic discount calculator. ### 3. Existing Code Patterns Show examples of how you write code in your project. This is closely related to the [few-shot-patterns](/lessons/few-shot-patterns) approach. **Poor prompt:** ``` Create an API endpoint for user registration ``` **Better prompt:** ``` Create an API endpoint for user registration following our existing pattern: // Example of our pattern: export async function POST(request: Request) { try { const body = await request.json(); const validated = schema.parse(body); const result = await service.execute(validated); return Response.json(result, { status: 200 }); } catch (error) { return handleError(error); } } Use Zod for validation. Schema should require: email, password (min 8 chars), name. ``` This ensures the generated code matches your established patterns, making it easier to maintain and review. For larger projects, check out [codebase-aware-prompting](/lessons/codebase-aware-prompting) for strategies to provide comprehensive codebase context. ### 4. Constraints and Limitations Be explicit about what the code should NOT do or must avoid. **Example prompt with constraints:** ``` Create a function to parse user input for a search query. Constraints: - Must sanitize input to prevent XSS attacks - Maximum query length: 200 characters - Strip all HTML tags - Preserve spaces and basic punctuation (. , ! ?) - No external dependencies - use vanilla JavaScript - Must work in IE11 (no modern ES6+ features) ``` ## Practical Examples: Before and After Let's look at real-world scenarios showing the impact of context and constraints. ### Example 1: Database Query Function **Without context:** ``` Write a function to get user data from the database ``` You might get generic SQL, a MongoDB query, or even a mock implementation. The AI has no idea what you're working with. **With context:** ``` Write a function to get user data using Prisma ORM with PostgreSQL. Context: - We have a User model with: id, email, name, createdAt, role - We have a Profile model related to User with: bio, avatar, userId - Function should return user with profile data included - Add type safety using TypeScript - Handle case where user doesn't exist - Don't expose password fields ``` **Result:** ```typescript import { prisma } from '@/lib/prisma'; import { User, Profile } from '@prisma/client'; type UserWithProfile = Omit & { profile: Profile | null; }; export async function getUserWithProfile( userId: string ): Promise { const user = await prisma.user.findUnique({ where: { id: userId }, select: { id: true, email: true, name: true, createdAt: true, role: true, password: false, // Explicitly exclude password profile: true, }, }); return user; } ``` The context ensures you get Prisma-specific code with proper type safety and security considerations. ### Example 2: Form Validation **Without context:** ``` Validate an email address ``` **With context:** ``` Create client-side email validation for a React form using react-hook-form and zod. Requirements: - Must be a valid email format - Must be from allowed domains: gmail.com, company.com, outlook.com - Show specific error messages for each validation failure - Integrate with our existing form structure that uses useForm hook ``` **Result:** ```typescript import { z } from 'zod'; import { useForm } from 'react-hook-form'; import { zodResolver } from '@hookform/resolvers/zod'; const ALLOWED_DOMAINS = ['gmail.com', 'company.com', 'outlook.com']; const emailSchema = z.object({ email: z .string() .min(1, 'Email is required') .email('Please enter a valid email address') .refine( (email) => { const domain = email.split('@')[1]; return ALLOWED_DOMAINS.includes(domain); }, { message: `Email must be from one of: ${ALLOWED_DOMAINS.join(', ')}`, } ), }); type EmailFormData = z.infer; export function EmailForm() { const { register, handleSubmit, formState: { errors }, } = useForm({ resolver: zodResolver(emailSchema), }); const onSubmit = (data: EmailFormData) => { console.log('Valid email:', data.email); }; return (
{errors.email && ( {errors.email.message} )}
); } ``` ## The Context-Constraint Template Here's a reusable template for structuring your prompts: ``` [TASK]: What you want to accomplish [TECHNICAL CONTEXT]: - Framework/library and version - Language and type system - Build tools or environment [BUSINESS REQUIREMENTS]: - What the code must do - Edge cases to handle - Expected inputs and outputs [PATTERNS TO FOLLOW]: - Code examples from your project - Naming conventions - File structure [CONSTRAINTS]: - What NOT to do - Performance requirements - Security considerations - Browser/environment compatibility [ADDITIONAL CONTEXT]: - Related code or dependencies - Known issues to avoid ``` ## Common Mistakes to Avoid As you develop your prompting skills, watch out for these pitfalls. For a comprehensive list, see [top-mistakes](/lessons/top-mistakes). ### 1. Assuming the AI Knows Your Stack Don't assume the AI will guess correctly. Even if you mentioned React earlier in the conversation, be explicit in each prompt. ### 2. Vague Requirements "Make it secure" or "optimize performance" are too vague. Specify: "Prevent SQL injection using parameterized queries" or "Use React.memo to prevent unnecessary re-renders." ### 3. Conflicting Constraints "Make it work in IE11 and use async/await" creates a conflict. Be aware when your constraints contradict each other. ### 4. Information Overload While context is important, there's a balance. Don't dump your entire codebase into a prompt. Focus on relevant context. Learn more about managing this in [context-window-management](/lessons/context-window-management). ## Iteration: Refining Context Based on Output Rarely will your first prompt be perfect. Part of vibe coding is iterating on your prompts. For detailed strategies, see [iterating-output](/lessons/iterating-output). **Initial prompt:** ``` Create a loading spinner component for React ``` **First output:** Generic spinner, not matching your design system. **Refined prompt:** ``` Update the spinner to match our design system: - Use our brand color: #3B82F6 - Size should be configurable: 'sm' (16px), 'md' (24px), 'lg' (48px) - Add aria-label for accessibility - Use CSS animation, not external libraries ``` Each iteration adds context based on what was missing in the previous output. ## Connecting Context to Your Workflow Providing context isn't just about a single prompt—it's part of your entire development workflow: 1. **Planning Phase**: When breaking down projects ([breaking-down-projects](/lessons/breaking-down-projects)), identify the context you'll need for each component. 2. **Implementation**: Use context-rich prompts for generation ([code-gen-best-practices](/lessons/code-gen-best-practices)). 3. **Review**: When reviewing generated code ([review-refactor](/lessons/review-refactor)), note what context was missing and adjust. 4. **Debugging**: When fixing errors ([error-resolution](/lessons/error-resolution)), provide context about the error state and expected behavior. ## Practice Exercise: Transform Your Prompts Take these vague prompts and add context based on a hypothetical e-commerce project using Next.js, TypeScript, Prisma, and PostgreSQL: 1. "Create a product card" 2. "Write a function to add items to cart" 3. "Make an API endpoint for checkout" For each, add: - Technical stack details - Business requirements - Constraints - Patterns to follow This exercise will help you internalize the context-first mindset essential for effective vibe coding. ## When Context Isn't Enough Sometimes, even with great context, AI-generated code isn't appropriate. Understanding [when-not-to-use-ai](/lessons/when-not-to-use-ai) is just as important as knowing how to use it effectively. Complex algorithms, security-critical code, or novel architectural decisions often benefit from human-first approaches. ## Key Takeaways - **Context is currency**: The more relevant context you provide, the better your results. - **Be specific**: Vague prompts get vague results. Specific prompts get specific, useful code. - **Use the four pillars**: Always include technical environment, business logic, code patterns, and constraints. - **Iterate intelligently**: Refine your prompts based on output quality. - **Think systematically**: Context-rich prompting is a skill that improves your entire development workflow. Mastering context and constraints sets the foundation for all advanced vibe coding techniques. As you progress through topics like [chain-of-thought](/lessons/chain-of-thought) prompting and [agentic workflows](/lessons/understanding-agentic), you'll build on these fundamentals. Start small: Take your next three AI coding prompts and apply the context-constraint template. Notice the difference in output quality. That's the power of effective prompting in action.