"How should I write specs for AI to get proper code?"
This is the question I get most often lately. ChatGPT, Claude, Copilot... Everyone's using them, but not getting the results they want.
I'll tell you a secret: Think of AI like a compiler.
Give a compiler code with wrong syntax? You get errors. AI is the same. Ambiguous specs produce ambiguous results.
Specs for Humans vs Specs for AI
Let's see the difference between 10 years ago and now.
10 Years Ago: Specs for Humans
Login Feature
- Login with email and password
- On success, go to dashboard
- On failure, show error
We expected developers to fill in the rest themselves.
Now: Specs for AI
Function: authenticateUser
Method: POST /api/v1/auth/login
Input:
email:
type: string
format: email
required: true
validation: RFC 5322
example: 'user@example.com'
password:
type: string
required: true
minLength: 8
maxLength: 128
validation:
- At least 1 uppercase
- At least 1 number
- At least 1 special character
Output:
success:
status: 200
body:
token: string (JWT)
expiresIn: number (seconds)
refreshToken: string
failure:
status: 401
body:
error: 'Invalid credentials'
code: 'AUTH_FAILED'
Business Logic: 1. Check email exists
2. Compare password with bcrypt
3. Record login attempt
4. Lock account after 5 failures
5. Issue JWT token (1 hour)
6. Issue refresh token (30 days)
Error Cases:
- Email not found: 404
- Wrong password: 401
- Account locked: 423
- Server error: 500
See the difference? AI has no implicit knowledge. You must specify everything.
5 Principles of AI-Friendly Specs
1. Structured
AI likes structure. Use formalized formats rather than free-form.
❌ Bad Example
When user logs in, give token and send to dashboard,
on failure show error message, and lock after 5 wrong attempts...
✅ Good Example
Steps: 1. Input validation
2. DB query
3. Password verification
4. Token generation
5. Return response
Error Handling:
- Case: Email not found
Action: Return 404
- Case: Wrong password
Action: Return 401, increment attempt count
2. Specific
Ambiguous expressions like "appropriately", "when needed", "generally" are forbidden.
❌ Ambiguous Spec
- Perform appropriate validation
- Cache when needed
- General error handling
✅ Specific Spec
- Email: RFC 5322 format validation
- Caching: Redis, TTL 3600 seconds
- Errors: 400 (validation), 401 (auth), 500 (server)
3. Include Examples
AI learns patterns through examples.
Examples:
Valid Input:
email: 'john@example.com'
password: 'SecurePass123!'
Expected Output: { 'token': 'eyJhbGciOiJIUzI1NiIs...', 'expiresIn': 3600, 'refreshToken': 'f47d3b2a-9c8e...' }
Invalid Input:
email: 'not-an-email'
password: '123'
Expected Error: { 'errors': ['Invalid email format', 'Password too short'] }
4. Specify Constraints
Clearly state technical and business constraints.
Constraints:
Technical:
- Node.js 18+
- Express 4.x
- PostgreSQL 14+
- JWT (not sessions)
Business:
- Password minimum 8 characters
- Token validity 1 hour
- Up to 3 simultaneous logins
- GDPR compliance
Performance:
- Response time < 200ms
- 1000 concurrent requests/s
5. Test Cases
Including test cases in specs makes AI generate more accurate code.
Test Cases:
- name: 'Normal login'
input: { email: 'test@test.com', password: 'Test123!' }
expected: { status: 200, hasToken: true }
- name: 'Invalid email'
input: { email: 'invalid', password: 'Test123!' }
expected: { status: 400, error: 'Invalid email' }
- name: 'Account locked'
setup: 'After 5 login failures'
input: { email: 'locked@test.com', password: 'Test123!' }
expected: { status: 423, error: 'Account locked' }
Practice: Writing Payment System Spec
A spec I used in an actual project.
# Payment Processing System Spec
## Overview
System: Payment Processing Service
Purpose: Subscription payment processing using Stripe
Version: 1.0.0
## Architecture
```
{% endraw %}
mermaid
graph LR
Client --> API
API --> Stripe
API --> Database
Stripe --> Webhook
Webhook --> API
{% raw %}
API Endpoints
1. Create Subscription
POST /api/v1/subscriptions
Request:
headers:
Authorization: Bearer {token}
body:
planId: string # "basic" | "pro" | "enterprise"
paymentMethodId: string # Stripe payment method
couponCode?: string
Response:
200 OK:
subscriptionId: string
status: "active" | "trialing"
currentPeriodEnd: ISO8601
nextBillingDate: ISO8601
400 Bad Request:
error: "Invalid plan"
code: "INVALID_PLAN"
402 Payment Required:
error: "Payment failed"
code: "PAYMENT_FAILED"
declineCode: string
Business Rules:
- Only 1 subscription per user allowed
- Upgrades apply immediately, downgrades at next billing date
- 3-day free trial (card registration required)
- 3 retries on failure (1 day, 3 days, 5 days)
Implementation Details:
- Create/query Stripe Customer
- Attach Payment Method
- Create Subscription
- Save to DB (subscriptions table)
- Send email notification
2. Cancel Subscription
DELETE /api/v1/subscriptions/{id}
Parameters:
id: string (subscription ID)
immediately?: boolean (default: false)
Response:
200 OK:
canceledAt: ISO8601
cancelAtPeriodEnd: boolean
Business Rules:
- Immediate cancel: No refund for remaining period
- Cancel at period end: Can use remaining period
3. Webhook Handler
POST /api/v1/webhooks/stripe
Security:
- Stripe signature verification required
- IP whitelist (optional)
Events:
customer.subscription.created: - Update DB status - Send welcome email
customer.subscription.updated: - Handle plan change - Send notification
customer.subscription.deleted: - Remove access rights - Send cancellation survey email
invoice.payment_failed: - Schedule retry - Send warning email
Database Schema
sql
CREATE TABLE subscriptions (
id UUID PRIMARY KEY,
user_id UUID REFERENCES users(id),
stripe_subscription_id VARCHAR(255) UNIQUE,
stripe_customer_id VARCHAR(255),
plan_id VARCHAR(50),
status VARCHAR(50),
current_period_start TIMESTAMP,
current_period_end TIMESTAMP,
cancel_at_period_end BOOLEAN DEFAULT false,
created_at TIMESTAMP DEFAULT NOW(),
updated_at TIMESTAMP DEFAULT NOW()
);
CREATE INDEX idx_user_subscription ON subscriptions(user_id);
CREATE INDEX idx_stripe_subscription ON subscriptions(stripe_subscription_id);
Error Handling
typescript
class PaymentError extends Error {
constructor(message: string, public code: string, public statusCode: number, public details?: any) {
super(message);
}
}
// Usage
throw new PaymentError('Payment method declined', 'PAYMENT_DECLINED', 402, { declineCode: 'insufficient_funds' });
Testing
Unit Tests
typescript
describe('SubscriptionService', () => {
it('should create subscription for new user', async () => {
// Given
const userId = 'user123';
const planId = 'pro';
// When
const subscription = await service.create(userId, planId);
// Then
expect(subscription.status).toBe('active');
expect(subscription.planId).toBe('pro');
});
it('should prevent duplicate subscriptions', async () => {
// Given: user with existing subscription
// When: attempt to create another
// Then: throw error
});
});
Integration Tests
- Use Stripe Test Mode
- Use Test card numbers
- Simulate Webhooks
Monitoring
Metrics:
- Payment success rate
- Average response time
- Retry count
- Cancellation rate
Alerts:
- Success rate < 95%
- Response time > 1 second
- 5xx errors > 1%
`
Giving AI specs like this produces production-level code.
## Tips for Writing Specs by AI Tool
### ChatGPT
- Improve incrementally through conversation
- Use "Based on previous code..."
- Context maintenance important
### Claude
- Provide full spec at once
- Handles long documents well
- Request structured output
### GitHub Copilot
- Write specs in comments
- Write function signature first
- More accurate if you write tests first
## Spec Template Collection
Templates I actually use:
### API Endpoint
```yaml
Endpoint: [METHOD] /path
Purpose: [One-line description]
Authentication: [Required/Optional]
Request: [Structure]
Response: [Structure]
Errors: [Error cases]
Business Logic: [Step by step]
````
### Data Model
```yaml
Model: [Name]
Table: [Table name]
Fields:
- name: type, constraints
Relations:
- type: target
Indexes:
- fields
Validations:
- rules
```
### Business Logic
```yaml
Function: [Name]
Input: [Parameters]
Output: [Return value]
Preconditions: [Preconditions]
Postconditions: [Postconditions]
Steps: [Algorithm]
Edge Cases: [Exception situations]
```
## Conclusion: Specs are an Investment
"Isn't it faster to code than write specs?"
Maybe in the past. But the AI era is different.
**30 minutes writing specs = 3 minutes AI code generation = 3 hours manual coding**
Writing good specs:
- AI generates accurate code
- Clear communication with team
- Automatic documentation completion
- Clear test cases
Specs are not a cost but an **investment**.
Especially when combined with WBS, it's even more powerful. Writing clear specs for each WBS task enables AI to implement almost the entire project automatically.
Next time you use AI, don't make a "rough" request. Write proper specs.
**You'll see amazing results.**
---
_Need project management and spec writing in the AI era? Check out [Plexo](https://plexo.work)._

Top comments (1)
Very interesting read. Some questions: what do you do with the "specs" once AI generated the code for you? Do you check them in? Do they become part of the codebase? Do you keep them around and if something needs to change you change the spec and let AI re-generate the code, or do you let the AI change the code following more "conversational" instructions? (not from a spec file) Thanks!