Code Quality Checklist

13 min read

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.example with dummy values
  • Add .env to .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

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 audit regularly
  • Update dependencies monthly
  • Remove unused packages
  • Use npm ci in 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.



Last updated: December 14, 2025