Code Quality Checklist
Content
Professional documentation and insights
Code Quality Checklist
Use this comprehensive checklist to evaluate AI-generated code before deploying to production. Whether you’re reviewing ChatGPT output, Copilot suggestions, or code from no-code tools, this guide helps ensure your application is professional, secure, and maintainable.
How to Use This Checklist
For Quick Audits: Review the summary checkboxes at the start of each section.
For Deep Reviews: Read the detailed explanations and examples under each item.
Priority Levels:
- CRITICAL - Must fix before production
- IMPORTANT - Fix before next release
- RECOMMENDED - Improve when possible
1. Security (CRITICAL)
Security vulnerabilities in AI-generated code can expose your application to attacks, data breaches, and compliance violations.
Quick Check
- No hardcoded secrets, API keys, or passwords
- All user inputs are validated and sanitized
- SQL queries use parameterization (no SQL injection risk)
- Authentication and authorization are implemented
- HTTPS is enforced in production
- Security headers are configured (CORS, CSP, HSTS)
- Dependencies are up-to-date with no known vulnerabilities
- Sensitive data is encrypted at rest and in transit
- File uploads are validated and restricted
- Rate limiting is implemented on API endpoints
Detailed Review
Hardcoded Secrets
The Problem: AI often generates code with hardcoded credentials because it doesn’t understand deployment security.
// ❌ Bad - Hardcoded secrets
const API_KEY = "sk-1234567890abcdefghijklmnop";
const DB_PASSWORD = "mypassword123";
const JWT_SECRET = "supersecret";
mongoose.connect("mongodb://admin:password123@localhost/myapp");
// ✅ Good - Environment variables
require("dotenv").config();
const API_KEY = process.env.API_KEY;
const DB_PASSWORD = process.env.DB_PASSWORD;
const JWT_SECRET = process.env.JWT_SECRET;
if (!API_KEY || !DB_PASSWORD || !JWT_SECRET) {
throw new Error("Required environment variables are missing");
}
mongoose.connect(process.env.DATABASE_URI);
Action Items:
- Move all secrets to environment variables
- Create
.env.examplewith dummy values - Add
.envto.gitignore - Use secret management tools (AWS Secrets Manager, Azure Key Vault)
- Rotate secrets regularly
Input Validation
The Problem: AI assumes all inputs are valid and well-formed.
// ❌ Bad - No validation
app.post("/api/users", (req, res) => {
const { email, age } = req.body;
const user = createUser(email, age);
res.json(user);
});
// ✅ Good - Validated inputs
const validator = require("validator");
app.post("/api/users", (req, res) => {
const { email, age } = req.body;
// Validate email
if (!email || !validator.isEmail(email)) {
return res.status(400).json({ error: "Invalid email address" });
}
// Validate age
if (!age || !Number.isInteger(age) || age < 18 || age > 120) {
return res.status(400).json({ error: "Age must be between 18 and 120" });
}
// Sanitize inputs
const sanitizedEmail = validator.normalizeEmail(email);
const user = createUser(sanitizedEmail, age);
res.json(user);
});
Action Items:
- Validate all user inputs (type, format, range)
- Sanitize data before processing
- Use validation libraries (Joi, Yup, Zod)
- Implement server-side validation (never trust client-side only)
- Whitelist allowed values when possible
SQL Injection Prevention
The Problem: AI often generates vulnerable SQL queries using string concatenation.
// ❌ Bad - SQL injection vulnerability
app.get("/users/:id", (req, res) => {
const query = `SELECT * FROM users WHERE id = ${req.params.id}`;
db.query(query, (err, result) => {
res.json(result);
});
});
// Attacker can send: /users/1%20OR%201=1
// Query becomes: SELECT * FROM users WHERE id = 1 OR 1=1
// ✅ Good - Parameterized queries
app.get("/users/:id", async (req, res) => {
const query = "SELECT * FROM users WHERE id = ?";
try {
const result = await db.query(query, [req.params.id]);
res.json(result);
} catch (error) {
console.error("Database error:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Or use an ORM
const user = await User.findByPk(req.params.id);
Action Items:
- Always use parameterized queries or prepared statements
- Use ORMs (Sequelize, TypeORM, Prisma) that handle this automatically
- Never concatenate user input into SQL strings
- Apply principle of least privilege to database users
- Use database query logging to detect suspicious patterns
Authentication and Authorization
The Problem: AI creates endpoints without proper access controls.
// ❌ Bad - No authentication
app.delete("/api/users/:id", async (req, res) => {
await deleteUser(req.params.id);
res.json({ success: true });
});
// Anyone can delete any user!
// ✅ Good - Authenticated and authorized
const authMiddleware = require("./middleware/auth");
const checkOwnership = require("./middleware/ownership");
app.delete(
"/api/users/:id",
authMiddleware, // Verify user is logged in
checkOwnership, // Verify user owns this resource
async (req, res) => {
try {
await deleteUser(req.params.id);
res.json({ success: true });
} catch (error) {
res.status(500).json({ error: error.message });
}
}
);
Action Items:
- Implement authentication (JWT, session-based, OAuth)
- Add authorization checks (user roles, permissions)
- Protect all sensitive endpoints
- Use middleware for consistent auth checks
- Implement rate limiting to prevent brute force attacks
2. Error Handling (CRITICAL)
Proper error handling prevents crashes, provides useful debugging information, and improves user experience.
Quick Check
- All async operations are wrapped in try-catch blocks
- Errors are logged with context (timestamp, user, action)
- Users receive helpful error messages (no stack traces)
- API endpoints return appropriate HTTP status codes
- Database connection errors are handled
- Third-party API failures are handled gracefully
- No empty catch blocks that swallow errors
- Unhandled promise rejections are caught
- Error monitoring is set up (Sentry, LogRocket)
Detailed Review
Try-Catch Blocks
The Problem: AI generates async code without error handling.
// ❌ Bad - No error handling
async function getUser(id) {
const response = await fetch(`/api/users/${id}`);
const user = await response.json();
return user;
}
// Crashes if network fails or API returns error
// ✅ Good - Comprehensive error handling
async function getUser(id) {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}: ${response.statusText}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error(`Failed to fetch user ${id}:`, error);
throw new Error("Unable to load user data. Please try again later.");
}
}
Action Items:
- Wrap all async operations in try-catch
- Check response status codes before processing
- Throw meaningful error messages
- Log errors with context
- Implement retry logic for transient failures
Error Responses
The Problem: AI returns inconsistent or insecure error responses.
// ❌ Bad - Exposes internal details
app.get("/api/data", async (req, res) => {
try {
const data = await fetchData();
res.json(data);
} catch (error) {
res.status(500).send(error.stack); // Exposes stack trace!
}
});
// ✅ Good - Consistent, safe error responses
app.get("/api/data", async (req, res) => {
try {
const data = await fetchData();
res.status(200).json({
success: true,
data: data,
});
} catch (error) {
console.error("Data fetch failed:", error); // Log internally
res.status(500).json({
success: false,
error: "Unable to fetch data at this time",
});
}
});
Action Items:
- Use consistent response format across all endpoints
- Return appropriate HTTP status codes
- Never expose stack traces or internal errors to users
- Log detailed errors server-side
- Create user-friendly error messages
3. Code Organization (IMPORTANT)
Well-organized code is easier to maintain, test, and scale.
Quick Check
- Code is separated into logical modules/files
- Folder structure follows a clear pattern (MVC, Clean Architecture)
- Functions are small and single-purpose (under 50 lines)
- No duplicate code (DRY principle)
- Business logic is separated from routes/controllers
- Configuration is centralized
- Constants and magic numbers are named
- Related code is grouped together
- File and folder names are descriptive
Detailed Review
File Structure
The Problem: AI puts everything in one massive file.
// ❌ Bad - Everything in app.js (2000+ lines)
app.js
// ✅ Good - Organized structure
/src
/config
database.js
app.js
/routes
user.routes.js
auth.routes.js
product.routes.js
/controllers
user.controller.js
auth.controller.js
product.controller.js
/models
user.model.js
product.model.js
/middleware
auth.middleware.js
validation.middleware.js
error.middleware.js
/services
email.service.js
payment.service.js
/utils
logger.js
validator.js
/tests
user.test.js
auth.test.js
app.js
server.js
Action Items:
- Separate routes, controllers, models, and services
- Keep files under 300 lines
- Group related functionality
- Use index files for clean imports
- Follow a consistent naming convention
Function Size and Purpose
The Problem: AI creates large, multi-purpose functions.
// ❌ Bad - Does too much
async function handleUserCreation(req, res) {
// Validate input
if (!req.body.email) return res.status(400).send("Email required");
if (!req.body.password) return res.status(400).send("Password required");
// Hash password
const salt = await bcrypt.genSalt(10);
const hashedPassword = await bcrypt.hash(req.body.password, salt);
// Create user
const user = new User({
email: req.body.email,
password: hashedPassword,
});
await user.save();
// Send welcome email
await sendEmail({
to: user.email,
subject: "Welcome!",
body: "Thanks for signing up",
});
// Generate token
const token = jwt.sign({ id: user._id }, process.env.JWT_SECRET);
res.json({ user, token });
}
// ✅ Good - Single responsibility functions
// Validation
function validateUserInput(data) {
const errors = [];
if (!data.email) errors.push("Email required");
if (!data.password) errors.push("Password required");
return errors;
}
// Password hashing
async function hashPassword(password) {
const salt = await bcrypt.genSalt(10);
return bcrypt.hash(password, salt);
}
// User creation service
async function createUser(userData) {
const hashedPassword = await hashPassword(userData.password);
const user = new User({
email: userData.email,
password: hashedPassword,
});
return user.save();
}
// Token generation
function generateAuthToken(userId) {
return jwt.sign({ id: userId }, process.env.JWT_SECRET);
}
// Controller
async function handleUserCreation(req, res) {
const errors = validateUserInput(req.body);
if (errors.length > 0) {
return res.status(400).json({ errors });
}
try {
const user = await createUser(req.body);
await sendWelcomeEmail(user.email);
const token = generateAuthToken(user._id);
res.status(201).json({ user, token });
} catch (error) {
console.error("User creation failed:", error);
res.status(500).json({ error: "Failed to create user" });
}
}
Action Items:
- Keep functions under 50 lines
- One function should do one thing
- Extract reusable logic into utilities
- Name functions clearly (verb + noun)
- Reduce nesting levels (max 3-4)
4. Performance (IMPORTANT)
Poorly optimized code can cause slow load times, high server costs, and poor user experience.
Quick Check
- No N+1 database query problems
- Database queries have appropriate indexes
- Large datasets use pagination
- Heavy operations are cached
- Images and assets are optimized
- API responses are compressed (gzip)
- Database connections use pooling
- Unnecessary data is not loaded
- Async operations run in parallel when possible
- Memory leaks are prevented (no unclosed connections)
Detailed Review
N+1 Query Problem
The Problem: AI makes separate database calls inside loops.
// ❌ Bad - N+1 queries
async function getUsersWithPosts() {
const users = await User.findAll();
for (let user of users) {
user.posts = await Post.findAll({ where: { userId: user.id } });
}
return users;
}
// Makes 1 query for users + N queries for posts = terrible performance
// ✅ Good - Single query with join
async function getUsersWithPosts() {
const users = await User.findAll({
include: [{ model: Post }],
});
return users;
}
// Or raw SQL
const query = `
SELECT u.*, p.id as post_id, p.title, p.content
FROM users u
LEFT JOIN posts p ON u.id = p.user_id
`;
const results = await db.query(query);
Action Items:
- Use database joins or eager loading
- Profile queries to identify N+1 problems
- Use query monitoring tools
- Add database indexes on foreign keys
- Consider using a query builder or ORM
Pagination
The Problem: AI loads all data at once without pagination.
// ❌ Bad - Loads everything
app.get("/api/products", async (req, res) => {
const products = await Product.findAll();
res.json(products); // Could be thousands of records
});
// ✅ Good - Paginated results
app.get("/api/products", async (req, res) => {
const page = parseInt(req.query.page) || 1;
const limit = parseInt(req.query.limit) || 20;
const offset = (page - 1) * limit;
const { count, rows } = await Product.findAndCountAll({
limit,
offset,
order: [["createdAt", "DESC"]],
});
res.json({
data: rows,
pagination: {
page,
limit,
total: count,
pages: Math.ceil(count / limit),
},
});
});
Action Items:
- Implement pagination for all list endpoints
- Set reasonable default page sizes (20-50 items)
- Return total count for UI pagination
- Add filtering and sorting options
- Consider cursor-based pagination for real-time data
Caching
The Problem: AI recalculates or refetches the same data repeatedly.
// ❌ Bad - Fetches on every request
app.get("/api/stats", async (req, res) => {
const stats = await calculateExpensiveStats(); // Takes 5 seconds
res.json(stats);
});
// ✅ Good - Cached with TTL
const NodeCache = require("node-cache");
const cache = new NodeCache({ stdTTL: 300 }); // 5 minute cache
app.get("/api/stats", async (req, res) => {
const cacheKey = "dashboard-stats";
let stats = cache.get(cacheKey);
if (!stats) {
stats = await calculateExpensiveStats();
cache.set(cacheKey, stats);
}
res.json(stats);
});
Action Items:
- Cache expensive computations
- Cache database query results
- Use Redis for distributed caching
- Set appropriate cache TTLs
- Implement cache invalidation strategy
5. Testing (IMPORTANT)
Untested code breaks silently and causes production issues.
Quick Check
- Unit tests exist for business logic
- Integration tests cover API endpoints
- Edge cases are tested (null, empty, invalid inputs)
- Error handling is tested
- Tests run automatically in CI/CD
- Test coverage is above 70%
- Tests are independent and can run in any order
- Mock external dependencies (APIs, databases)
- Test data fixtures are available
Detailed Review
Unit Tests
The Problem: AI doesn’t write tests. Code works once, then breaks.
// ❌ Bad - No tests
function calculateDiscount(price, discountPercent) {
return price - (price * discountPercent) / 100;
}
// ✅ Good - Comprehensive tests
describe("calculateDiscount", () => {
it("should calculate 10% discount correctly", () => {
expect(calculateDiscount(100, 10)).toBe(90);
});
it("should handle 0% discount", () => {
expect(calculateDiscount(100, 0)).toBe(100);
});
it("should handle 100% discount", () => {
expect(calculateDiscount(100, 100)).toBe(0);
});
it("should throw error for negative discount", () => {
expect(() => calculateDiscount(100, -10)).toThrow();
});
it("should throw error for discount > 100", () => {
expect(() => calculateDiscount(100, 150)).toThrow();
});
});
Action Items:
- Write tests for all business logic
- Test happy paths and edge cases
- Test error conditions
- Aim for 70-80% code coverage
- Run tests before every commit
Integration Tests
The Problem: AI doesn’t test how components work together.
// ✅ Good - API endpoint test
const request = require("supertest");
const app = require("../app");
describe("POST /api/users", () => {
it("should create a new user", async () => {
const response = await request(app).post("/api/users").send({
email: "test@example.com",
password: "password123",
});
expect(response.status).toBe(201);
expect(response.body.user).toHaveProperty("id");
expect(response.body.user.email).toBe("test@example.com");
});
it("should reject invalid email", async () => {
const response = await request(app).post("/api/users").send({
email: "invalid-email",
password: "password123",
});
expect(response.status).toBe(400);
expect(response.body.error).toContain("email");
});
});
Action Items:
- Test API endpoints end-to-end
- Test database operations
- Test authentication flows
- Use test databases (separate from production)
- Clean up test data after each test
6. Documentation (RECOMMENDED)
Good documentation helps other developers (and future you) understand the code.
Quick Check
- README.md explains project setup
- API endpoints are documented
- Environment variables are documented
- Complex functions have comments
- Database schema is documented
- Deployment process is documented
- Dependencies are explained in package.json
- Code comments explain “why”, not “what”
Detailed Review
README.md
The Problem: AI doesn’t create documentation.
# ✅ Good README.md
# Project Name
Brief description of what the project does.
## Prerequisites
- Node.js 18+
- MongoDB 6+
- Redis (optional, for caching)
## Installation
```bash
# Clone the repository
git clone https://github.com/user/project.git
# Install dependencies
npm install
# Copy environment variables
cp .env.example .env
# Edit .env with your values
nano .env
# Run database migrations
npm run migrate
# Start development server
npm run dev
```
Environment Variables
| Variable | Description | Default | Required |
|---|---|---|---|
| PORT | Server port | 3000 | No |
| DATABASE_URI | MongoDB connection | localhost | Yes |
| JWT_SECRET | JWT signing key | - | Yes |
| REDIS_URL | Redis connection | - | No |
API Documentation
See API.md for endpoint documentation.
Testing
# Run all tests
npm test
# Run with coverage
npm run test:coverage
Deployment
See DEPLOYMENT.md for deployment instructions.
License
MIT
**Action Items:**
- Create comprehensive README
- Document setup process
- List all environment variables
- Explain project structure
- Include troubleshooting section
---
## 7. Dependencies (RECOMMENDED)
Outdated or unnecessary dependencies increase security risks and bundle size.
### Quick Check
- [ ] Dependencies are up-to-date
- [ ] No known security vulnerabilities
- [ ] Unused dependencies are removed
- [ ] Package versions are pinned or use ranges wisely
- [ ] Dependencies are regularly audited
- [ ] License compatibility checked
- [ ] Bundle size is reasonable
### Detailed Review
```bash
# Check for vulnerabilities
npm audit
# Update dependencies
npm update
# Remove unused dependencies
npm prune
# Check for outdated packages
npm outdated
Action Items:
- Run
npm auditregularly - Update dependencies monthly
- Remove unused packages
- Use
npm ciin CI/CD for reproducible builds - Consider bundle size impact
Quick Audit Script
Use this script to quickly audit your codebase:
#!/bin/bash
echo "Running Code Quality Audit..."
echo "=============================="
# Security
echo "Checking for security vulnerabilities..."
npm audit
# Code quality
echo "Running linter..."
npm run lint
# Tests
echo "Running tests..."
npm test
# Check for hardcoded secrets
echo "Checking for hardcoded secrets..."
grep -r "password\|secret\|api_key" --include="*.js" --exclude-dir=node_modules .
# Check for console.logs
echo "Checking for console.logs..."
grep -r "console.log" --include="*.js" --exclude-dir=node_modules .
echo "=============================="
echo "Audit complete!"
Priority Matrix
Fix Immediately (Production Blockers):
- Hardcoded secrets
- SQL injection vulnerabilities
- Missing authentication
- Unhandled errors causing crashes
Fix Before Launch:
- Input validation
- Error logging
- Security headers
- N+1 query problems
Improve Over Time:
- Test coverage
- Documentation
- Code organization
- Performance optimization
Need Professional Review?
If you’re unsure about your code quality or need expert eyes on your AI-generated application, we specialize in comprehensive code audits and fixes.
Get in Touch to schedule a code review.
Related Guides
- AI Code Fixing Guides - Comprehensive fixing strategies
- Debugging Common AI Mistakes - Debugging techniques
Last updated: December 14, 2025