TL;DR: Run this checklist on every API before shipping: (1) Auth on every endpoint, (2) input validation before DB queries, (3) rate limiting on auth endpoints, (4) restrictive CORS, (5) no secrets in code, (6) sanitized error messages, (7) HTTPS-only cookies with SameSite. AI misses at least one of these on nearly every generated API.
The Pre-Ship Security Checklist
1. Authentication on Every Sensitive Endpoint
// ❌ Common AI pattern: only checks auth globally, misses specific routes
app.use(authMiddleware) // Applied globally...
app.get('/api/users/:id', getUserHandler) // ...but what if authMiddleware has an exception?
// ✅ Better: explicit auth check in each handler
app.get('/api/users/:id', requireAuth, async (req, res) => {
// Only reaches here if authenticated
const user = await db.user.findUnique({ where: { id: req.params.id } })
if (user.id !== req.user.id && req.user.role !== 'admin') {
return res.status(403).json({ error: 'Forbidden' })
}
res.json(user)
})
Check every endpoint: does it require authentication? Does it verify the authenticated user is authorized to access this specific resource? The second check (object-level authorization) is what AI most often skips.
2. Input Validation
Never trust user input. Validate before using in queries, emails, or logic:
import { z } from 'zod'
const CreateUserSchema = z.object({
email: z.string().email(),
name: z.string().min(1).max(100),
role: z.enum(['user', 'admin']).default('user'),
})
app.post('/api/users', requireAuth, async (req, res) => {
const result = CreateUserSchema.safeParse(req.body)
if (!result.success) {
return res.status(400).json({ error: result.error.flatten() })
}
const { email, name, role } = result.data // safe, validated data
// ...proceed with DB operation
})
Zod is the validation library of choice in TypeScript projects. If AI does not add it, ask it to: "Add Zod validation to this endpoint before the database operation."
3. Rate Limiting
Without rate limiting, your login endpoint can be brute-forced and your API can be abused:
import rateLimit from 'express-rate-limit'
// Strict limit on auth endpoints
const authLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 10, // 10 attempts per window
message: { error: 'Too many attempts. Try again in 15 minutes.' },
standardHeaders: true,
})
app.use('/api/auth', authLimiter)
// General API limit
const apiLimiter = rateLimit({ windowMs: 60 * 1000, max: 100 })
app.use('/api', apiLimiter)
4. Restrictive CORS
// ❌ AI often generates this
app.use(cors()) // Allows ALL origins
// ✅ Restrict to your own frontend
app.use(cors({
origin: process.env.NODE_ENV === 'production'
? 'https://myapp.com'
: 'http://localhost:3000',
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE'],
}))
5. No Secrets in Code
Search your entire codebase before every commit:
# Find potential hardcoded secrets
grep -rn "sk_live\|sk_test\|api_key\|apiKey\|secret\|password" --include="*.ts" --include="*.js" . \
| grep -v "node_modules" \
| grep -v ".env"
Every secret must be in .env.local (local) or your deployment platform's secrets manager (production). Verify .env* is in .gitignore.
6. Sanitized Error Messages
// ❌ Exposes stack trace and internal details
app.use((err, req, res, next) => {
res.status(500).json({ error: err.message, stack: err.stack })
})
// ✅ Generic message in production, details in logs only
app.use((err, req, res, next) => {
console.error(err) // Log the full error server-side
res.status(err.status || 500).json({
error: process.env.NODE_ENV === 'production'
? 'Internal server error'
: err.message
})
})
7. Secure Cookie Configuration
// Every auth-related cookie should have all four:
res.cookie('session', token, {
httpOnly: true, // No JS access
secure: process.env.NODE_ENV === 'production', // HTTPS only
sameSite: 'Lax', // CSRF protection
maxAge: 7 * 24 * 60 * 60 * 1000, // Explicit expiry
})
The 10-Minute Security Audit
Before deploying any AI-generated API, run through these grep searches:
# 1. Find API routes without auth checks
grep -rn "app.get\|app.post\|app.put\|app.delete" --include="*.ts" .
# 2. Find cookies without sameSite
grep -rn "res.cookie" --include="*.ts" . | grep -v "sameSite"
# 3. Find potential hardcoded secrets
grep -rn "= ['\"]sk_\|= ['\"]key_\|= ['\"]secret" --include="*.ts" .
# 4. Find CORS allowing all origins
grep -rn "cors()" --include="*.ts" .
# 5. Find SQL or query injection risks
grep -rn "raw\|$\{req\.\|template literal.*req\." --include="*.ts" .
What to Learn Next
- What Is SQL Injection? — Input validation prevents this; understand why it matters.
- What Is XSS? — Client-side complement to API security.
- What Is CSRF? — Cookie security and cross-site request attacks.
- What Is a JWT? — Auth tokens and their security properties.
Before You Ship
Run the 10-minute audit on your current project right now. Paste the grep results into Claude and ask: "Review these routes for missing authentication and input validation." This human-AI collaboration catches what purely automated tools and purely AI generation both miss.
FAQ
Essential checklist: authentication on every sensitive endpoint, Zod input validation before any DB operation, rate limiting on auth routes, restrictive CORS configuration, all secrets in environment variables, sanitized error messages in production, and secure cookie attributes (httpOnly + sameSite + secure).
Most common omissions: missing object-level authorization checks (verifying the user owns the resource, not just is logged in), no Zod input validation, CORS configured with * (all origins), no rate limiting on auth endpoints, verbose error messages exposing stack traces, and missing sameSite/secure cookie attributes.
Object-level authorization on every endpoint. Authentication (proving who you are) is not enough — you also need authorization (proving you can access THIS specific resource). AI often adds auth middleware globally but skips the per-resource ownership check inside each handler.