Constraints & Guardrails
Learning Outcomes
- Write positive constraints (DO use) and negative constraints (DO NOT) effectively
- Specify output format to get code structured exactly as needed
- Set appropriate length and complexity bounds for generated code
- Apply production guardrails for code that handles accounts, data and APIs
- Use the "boundary" approach to define the edges of acceptable output
Lesson Plan
| Segment | Duration | Topic |
|---|---|---|
| Intro | 3 min | Why AI needs boundaries |
| Explain | 8 min | Positive vs negative constraints |
| Demo | 7 min | Output format specification |
| Explain | 7 min | Length and complexity bounds |
| Demo | 8 min | Production rails for code generation |
| Practice | 5 min | The boundary approach in action |
| Wrap-up | 2 min | Key takeaways, preview next lesson |
Before You Begin
Pre-work:
- Complete Lessons 1-4 (Anatomy through Few-Shot)
- Think of a time AI generated code that was technically correct but wrong for your context
- Review your project's coding standards or linting rules
Shopping List:
- Any AI coding tool (Claude Code, Cursor, or Codex CLI)
- Your project's linting configuration (ESLint, Pylint, etc.) for reference
- A list of "things AI should never do" in your codebase
Constraints tell AI what the boundaries of acceptable output are. They come in two forms:
Positive constraints (DO use): Tell AI what TO do, what TO include, what patterns TO follow.
Constraints:
- Use TypeScript strict mode (no implicit any)
- Handle all error cases with custom error classes
- Include JSDoc comments on all public methods
- Use dependency injection for all external services
- Return immutable objects (use Readonly<T> or Object.freeze)
Negative constraints (DO NOT): Tell AI what to AVOID, what NOT to include, what anti-patterns to skip.
Constraints:
- Do NOT use "any" type anywhere
- Do NOT mutate function parameters
- Do NOT use console.log (use the logger service instead)
- Do NOT use synchronous file system operations
- Do NOT import from internal/private modules
Which type is more effective?
Both have their place, but positive constraints are generally stronger. Here's why:
- Positive: "Use parameterised queries for all database access" — clear, actionable, unambiguous
- Negative: "Don't concatenate user input into SQL strings" — tells AI what to avoid but doesn't specify the alternative
The ideal approach combines both — positive constraints for the primary path, negative constraints for known pitfalls:
Database access rules:
- Use parameterised queries for ALL user-provided values
- Use the query builder (knex) rather than raw SQL where possible
- Do NOT concatenate variables into SQL strings
- Do NOT use SELECT * — always specify columns
- Do NOT run queries outside a transaction for multi-step operations
Common positive constraints for code generation:
- Use early returns to avoid nested if-else
- Extract magic numbers into named constants
- Keep functions under 30 lines
- Use descriptive variable names (no single letters except loop counters)
- Handle the loading, error, and empty states for all data fetching
Common negative constraints for code generation:
- Do NOT use var (use const/let)
- Do NOT use .then() chains (use async/await)
- Do NOT disable linting rules with comments
- Do NOT use eval() or Function() constructor
- Do NOT store secrets in source code
One of the most powerful constraints is telling AI exactly what shape the output should take. Without format specification, AI makes assumptions about file structure, export style, and organisation.
Specifying file structure:
Create the user validation module with this structure:
File: src/validators/user.validator.ts
- Export a validateCreateUser function
- Export a validateUpdateUser function
- Export the validation schemas as named exports
- Do NOT export a default
Import validation library: use zod
Import types from: ../types/user.types.ts
Specifying function signatures:
The function must have this exact signature:
function processPayment(
orderId: string,
amount: MoneyAmount,
method: PaymentMethod,
options?: PaymentOptions
): Promise<PaymentResult>
Where:
- MoneyAmount = { value: number; currency: CurrencyCode }
- PaymentResult = { success: boolean; transactionId?: string; error?: PaymentError }
Implement the body. Do not change the signature.
Specifying response shapes:
The API endpoint must return responses in this exact format:
Success (200):
{
"data": { ... },
"meta": {
"requestId": "uuid",
"timestamp": "ISO-8601"
}
}
Error (4xx/5xx):
{
"error": {
"code": "SNAKE_CASE_ERROR_CODE",
"message": "Human-readable message",
"details": [ ... ] // optional, for validation errors
},
"meta": {
"requestId": "uuid",
"timestamp": "ISO-8601"
}
}
Specifying code organisation:
Organise the code in this order within the file:
1. Imports (external libraries first, then internal modules)
2. Type definitions and interfaces
3. Constants
4. Helper functions (private, not exported)
5. Main exported functions
6. Default export (if applicable)
The "looks like this" shortcut:
When describing format is tedious, show a skeleton:
Generate the component following this skeleton:
import { ... } from '...';
import styles from './ComponentName.module.css';
interface ComponentNameProps {
// props here
}
export const ComponentName: FC<ComponentNameProps> = ({ ...props }) => {
// hooks at the top
// handlers in the middle
// return JSX at the bottom
return (
<div className={styles.container}>
{/* content */}
</div>
);
};
This is a hybrid of format specification and one-shot example — it shows structure without implementation.
AI tends toward verbosity. Without bounds, it will often produce code that's longer, more abstracted, and more complex than necessary. Setting explicit bounds prevents over-engineering.
Length constraints:
Keep the implementation simple:
- Maximum 50 lines for the main function
- No more than 3 levels of nesting
- If a helper is needed, it should be under 20 lines
- Total file should not exceed 150 lines
Write a MINIMAL implementation:
- Solve the core problem with the least code possible
- No error handling beyond what's critical for data integrity
- No logging, metrics, or observability (we'll add that later)
- No configuration — hardcode values, we'll extract them next
Complexity constraints:
Keep complexity low:
- No generics unless absolutely necessary for type safety
- No more than 2 type parameters on any function
- Prefer simple if/else over complex ternaries
- Use a switch statement instead of object lookup if there are fewer than 5 cases
- No metaprogramming (no Proxy, Reflect, or dynamic property access)
Avoid premature abstraction:
- Do NOT create a base class
- Do NOT add a factory pattern
- Do NOT create interfaces for things with only one implementation
- Just write the concrete implementation directly
- We'll abstract later if a second use case emerges
When to set bounds explicitly:
- Prototyping: "Give me the simplest possible version. No error handling, no edge cases, just the happy path in under 30 lines."
- Production: "Full implementation with error handling, input validation, logging, and comprehensive JSDoc comments."
- Learning: "Simple implementation with detailed comments explaining each decision."
Scaling constraints based on task phase:
Phase 1 (Prototype):
Build the feature with these limits:
- Happy path only
- In-memory storage (no database)
- No authentication check
- Console.log instead of proper logging
Phase 2 (Harden):
Now add production concerns:
- Proper error handling for all failure modes
- Replace in-memory with PostgreSQL queries
- Add auth middleware check
- Replace console.log with structured logger
The "just enough" principle:
Implement this with the minimum viable complexity:
- If a function works, don't wrap it in a class
- If a class works, don't add an interface
- If an interface works, don't add a generic
- If a generic works, don't add a higher-kinded type
Only add abstraction when there's a concrete second use case, not
"in case we need it later."
Certain categories of code need explicit constraints. Without them, AI might produce code that works but leaks data, fails badly, or ships with dangerous defaults.
Accounts and sessions:
Constraints for this sign-in endpoint:
- NEVER return the password hash in any response
- NEVER log tokens, passwords, or session IDs
- ALWAYS set HttpOnly and Secure flags on cookies
- ALWAYS regenerate session ID after successful login
- Rate limit to 5 attempts per minute per IP
- Lock account after 10 failed attempts
Data handling:
Data safety constraints:
- NEVER include PII (email, name, phone) in log messages
- NEVER store unencrypted sensitive data (SSN, card numbers)
- ALWAYS sanitise user input before database insertion
- ALWAYS validate file uploads (type, size, content)
- ALWAYS use parameterised queries, never string interpolation
API design:
API safety constraints:
- NEVER expose internal IDs in public-facing responses (use UUIDs)
- NEVER return stack traces in production error responses
- ALWAYS validate Content-Type header matches expected format
- ALWAYS set appropriate CORS headers (not wildcard in production)
- ALWAYS paginate list endpoints (max 100 items per page)
- Include rate limiting headers (X-RateLimit-Remaining)
Dependency and environment:
Environment constraints:
- Do NOT read environment variables directly — use the config service
- Do NOT install new dependencies without listing them
- Do NOT use eval(), new Function(), or child_process.exec with user input
- Do NOT write to the filesystem outside the designated temp directory
- Do NOT make network requests to URLs provided by users without validation
Building a project rules template:
Collect your constraints into a reusable template that you include at the start of any prompt that touches accounts or data:
## Project Rules (include in all account/data prompts)
1. Input: validate and sanitise all user input at the boundary
2. Output: never expose internal errors, IDs, or system details
3. Storage: encrypt at rest, parameterise queries, no raw SQL
4. Transport: HTTPS only, secure cookie flags, proper CORS
5. Logging: no PII, no secrets, no tokens in logs
6. Dependencies: pin versions, audit regularly, no eval
The boundary approach defines the acceptable space for AI output by specifying what's in bounds and what's out of bounds. Rather than micromanaging the implementation, you define the edges.
The pattern:
Build [X] within these boundaries:
IN SCOPE:
- [What the code SHOULD handle]
- [What cases it SHOULD cover]
- [What technologies it SHOULD use]
OUT OF SCOPE:
- [What to explicitly NOT handle]
- [What to defer to a later iteration]
- [What's another team's responsibility]
QUALITY BOUNDARY:
- [Minimum acceptable quality]
- [Maximum acceptable complexity]
Example: Building a search feature
Build a product search API endpoint within these boundaries:
IN SCOPE:
- Full-text search by product name and description
- Filter by: category, price range, in-stock status
- Sort by: relevance, price (asc/desc), newest
- Pagination with cursor-based navigation
- Response time under 200ms for typical queries
OUT OF SCOPE:
- Typo correction / "did you mean" suggestions (Phase 2)
- Search analytics / tracking (separate service)
- Personalised results (requires ML pipeline, not yet built)
- Image-based search
- Geo-location filtering
QUALITY BOUNDARY:
- Minimum: works correctly for all filter combinations, handles empty results
- Maximum: don't build a custom search engine — use PostgreSQL full-text
search. If we need Elasticsearch later, we'll migrate.
Example: Building a form component
Build a multi-step checkout form within these boundaries:
IN SCOPE:
- Steps: shipping address, payment method, review & confirm
- Field validation on blur and on submit
- Progress indicator showing current step
- Ability to go back to previous steps (preserving data)
- Submit button disabled until current step is valid
OUT OF SCOPE:
- Address autocomplete (third-party API, different ticket)
- Saved payment methods (requires vault integration)
- Guest vs logged-in user differences (treat all as logged in for now)
- Mobile-specific layout (responsive CSS is fine, no separate mobile UX)
QUALITY BOUNDARY:
- Minimum: all fields validate, steps navigate correctly, data submits
- Maximum: don't build a generic multi-step form framework. Build THIS
checkout form. If we need another multi-step form, we'll extract shared
logic then.
Why the boundary approach works:
- Prevents scope creep — AI won't add "nice to have" features you'll need to maintain
- Sets expectations — you won't be surprised by what's missing
- Enables iteration — OUT OF SCOPE items become your next prompt
- Communicates intent — future developers (including yourself) understand what was deliberately excluded
Combining boundaries with other constraint types:
Build the notification service within these boundaries:
IN SCOPE: email and push notifications for order status changes
OUT OF SCOPE: SMS, in-app notifications, notification preferences UI
Positive constraints:
- Use the existing EmailService and PushService interfaces
- Queue notifications for async delivery (use Bull/Redis)
- Include retry logic (3 attempts, exponential backoff)
Negative constraints:
- Do NOT send notifications synchronously in the request path
- Do NOT store notification content in the database (just delivery status)
- Do NOT add new dependencies — use what's already in package.json
Format:
- One file: src/services/notification.service.ts
- Export a single NotificationService class
- Methods: notifyOrderShipped, notifyOrderDelivered, notifyOrderCancelled
This combines boundaries (scope), positive constraints (what to do), negative constraints (what to avoid), and format specification (structure) into a comprehensive but clear prompt.
Questions & Answers
Key Takeaways
- Positive constraints guide, negative constraints warn — use both, but lead with what you want (not just what you don't want).
- Format specification eliminates integration friction — tell AI exactly what shape the output should take.
- Set complexity bounds explicitly — without them, AI defaults to over-engineering.
- Safety constraints should be loud and repetitive — never assume AI will "know" to be secure.
- The boundary approach defines the space — IN SCOPE, OUT OF SCOPE, and QUALITY BOUNDARY together prevent scope creep and set clear expectations.
Next Steps: In Lesson 6 — System Prompts & Rules Files, we'll learn how to encode your constraints permanently so they apply to every AI interaction in your project without repetition.