Skip to content

Code Quality

Seed maintains high code quality standards through automated tools and conventions. This guide covers linting, formatting, type checking, and best practices.

Overview

ToolPurposeConfiguration
TypeScriptType safety and modern JavaScript featurestsconfig.json
ESLintCode linting and error detectioneslint.config.js
PrettierCode formatting and style consistency.prettierrc

TypeScript

Configuration

File: tsconfig.json

json
{
  "compilerOptions": {
    "target": "ES2022",
    "module": "NodeNext",
    "moduleResolution": "NodeNext",
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "declaration": true,
    "declarationMap": true,
    "sourceMap": true,
    "resolveJsonModule": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": true,
    "noFallthroughCasesInSwitch": true,
    "exactOptionalPropertyTypes": true,
    "noUncheckedIndexedAccess": true
  }
}

Strict Mode Features

strict: true enables:

  • strictNullChecks - No implicit null/undefined
  • strictFunctionTypes - Strict function type checking
  • strictBindCallApply - Strict bind/call/apply
  • strictPropertyInitialization - Properties must be initialized
  • noImplicitAny - No implicit any types
  • noImplicitThis - No implicit this
  • alwaysStrict - Use strict mode in all files

Additional strict checks:

  • noUnusedLocals - Error on unused local variables
  • noUnusedParameters - Error on unused parameters
  • noImplicitReturns - All code paths must return
  • noFallthroughCasesInSwitch - No switch fallthrough
  • exactOptionalPropertyTypes - Optional properties cannot be undefined
  • noUncheckedIndexedAccess - Array/object access returns T | undefined

Module System

Uses ES Modules (ESM) with .js extensions:

typescript
// Correct: .js extension required
import { config } from "./config/index.js";

// Incorrect: TypeScript will error
import { config } from "./config/index";

Why .js extensions?

  • Node.js requires extensions for ESM
  • TypeScript compiles .ts.js
  • Import paths must reference the runtime file extension

Type Safety Best Practices

1. Avoid any

typescript
// Bad
function process(data: any) {
  return data.value;
}

// Good
interface Data {
  value: string;
}

function process(data: Data): string {
  return data.value;
}

2. Use type guards

typescript
function isString(value: unknown): value is string {
  return typeof value === "string";
}

function process(input: unknown) {
  if (isString(input)) {
    console.log(input.toUpperCase()); // TypeScript knows input is string
  }
}

3. Handle array access safely

typescript
// With noUncheckedIndexedAccess
const items = ["a", "b", "c"];
const first = items[0]; // Type: string | undefined

// Must handle undefined
if (first) {
  console.log(first.toUpperCase());
}

4. Use strict null checks

typescript
interface User {
  name: string;
  email?: string; // Explicitly optional
}

function getEmail(user: User): string {
  // Error: email might be undefined
  // return user.email;

  // Correct: handle undefined
  return user.email ?? "no-email@example.com";
}

Running Type Checks

bash
# Check types without emitting files
npm run typecheck

# Check and emit files
npm run build

ESLint

Configuration

File: eslint.config.js

javascript
export default tseslint.config(
  eslint.configs.recommended,
  ...tseslint.configs.strictTypeChecked,
  ...tseslint.configs.stylisticTypeChecked,
  prettier,
  {
    ignores: ["dist/**", "node_modules/**", "coverage/**"],
  },
  {
    files: ["src/**/*.ts"],
    languageOptions: {
      parserOptions: {
        project: true,
        tsconfigRootDir: import.meta.dirname,
      },
    },
    rules: {
      "@typescript-eslint/no-unused-vars": [
        "error",
        { argsIgnorePattern: "^_" },
      ],
      "@typescript-eslint/consistent-type-imports": "error",
      "@typescript-eslint/no-import-type-side-effects": "error",
    },
  },
  {
    files: ["src/**/*.test.ts"],
    rules: {
      "@typescript-eslint/unbound-method": "off",
    },
  }
);

Key Rules

Unused variables:

typescript
// Error: 'unused' is defined but never used
const unused = "value";

// OK: Prefixed with underscore
function handler(_req: Request, res: Response) {
  // _req is intentionally unused
  res.send("ok");
}

Consistent type imports:

typescript
// Error: Use type import
import { Request } from "express";

// Correct: Type-only import
import type { Request } from "express";

No import side effects:

typescript
// Error: Mixing type and value imports
import { type Request, Router } from "express";

// Correct: Separate imports
import type { Request } from "express";
import { Router } from "express";

Running ESLint

bash
# Lint all source files
npm run lint

# Lint specific file
npx eslint src/routes/health.ts

# Auto-fix issues
npx eslint src/routes/health.ts --fix

Disabling Rules

Inline disable (use sparingly):

typescript
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function legacy(data: any) {
  // ...
}

/* eslint-disable @typescript-eslint/no-floating-promises */
void someAsyncFunction();
/* eslint-enable @typescript-eslint/no-floating-promises */

Prettier

Configuration

File: .prettierrc

json
{
  "semi": true,
  "singleQuote": false,
  "tabWidth": 2,
  "trailingComma": "all",
  "printWidth": 100,
  "endOfLine": "lf"
}

Style Guidelines

Semicolons: Required

typescript
const value = "test"; // ✓
const value = "test"  // ✗

Quotes: Double quotes

typescript
const name = "value"; // ✓
const name = 'value'; // ✗

Tab width: 2 spaces

typescript
function example() {
  if (true) {
    return "formatted"; // 2 spaces per level
  }
}

Trailing commas: Always

typescript
const obj = {
  a: 1,
  b: 2, // ✓ Trailing comma
};

const arr = [
  "a",
  "b", // ✓ Trailing comma
];

Line width: 100 characters

typescript
// ✓ Under 100 chars
const short = "This line is short enough to fit";

// ✗ Over 100 chars - Prettier will break it
const long = "This is a very long line that exceeds one hundred characters and should be broken";

Running Prettier

bash
# Format all source files
npm run format

# Check formatting without changing files
npm run format:check

# Format specific file
npx prettier --write src/routes/health.ts

IDE Integration

VS Code (.vscode/settings.json):

json
{
  "editor.formatOnSave": true,
  "editor.defaultFormatter": "esbenp.prettier-vscode"
}

WebStorm:

  • Settings → Languages & Frameworks → JavaScript → Prettier
  • Check "On save" and "On code reformat"

Validation Workflow

Pre-Commit Checklist

Before committing code:

bash
# Run full validation
npm run validate

This runs:

  1. Prettier (npm run format) - Format code
  2. ESLint (npm run lint) - Lint code
  3. TypeScript (npm run typecheck) - Check types
  4. Tests (npm run test:coverage) - Run tests with coverage

CI/CD Validation

The same validation runs in GitLab CI:

yaml
# .gitlab-ci.yml (example)
validate:
  script:
    - npm run validate

Common Issues

1. Import Extension Errors

Error:

Relative import paths need explicit file extensions in ESM imports

Solution:

typescript
// Wrong
import { config } from "./config";

// Correct
import { config } from "./config.js";

2. Type vs Value Imports

Error:

'Request' is a type and must be imported using a type-only import

Solution:

typescript
// Wrong
import { Request } from "express";

// Correct
import type { Request } from "express";

3. Unused Variables

Error:

'value' is defined but never used

Solutions:

typescript
// Option 1: Remove unused variable
// const value = "unused"; // Remove this line

// Option 2: Use underscore prefix
const _value = "intentionally unused";

// Option 3: Disable rule (rare cases)
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const value = "required by interface";

4. Implicit Any

Error:

Parameter 'data' implicitly has an 'any' type

Solution:

typescript
// Wrong
function process(data) {
  return data;
}

// Correct
function process(data: unknown) {
  // Use type guards to narrow type
  if (typeof data === "string") {
    return data.toUpperCase();
  }
  return data;
}

5. Null/Undefined Checks

Error:

Object is possibly 'undefined'

Solution:

typescript
const items = ["a", "b", "c"];

// Wrong
console.log(items[10].toUpperCase()); // Error

// Correct: Optional chaining
console.log(items[10]?.toUpperCase());

// Correct: Explicit check
const item = items[10];
if (item) {
  console.log(item.toUpperCase());
}

// Correct: Nullish coalescing
const value = items[10] ?? "default";

Code Review Guidelines

What to Look For

1. Type safety:

  • No any types (use unknown instead)
  • Proper type annotations on functions
  • Type guards for runtime validation

2. Error handling:

  • Try-catch for async operations
  • Meaningful error messages
  • Proper error types

3. Code organization:

  • Single responsibility per function
  • Clear naming conventions
  • Appropriate file organization

4. Testing:

  • Unit tests for new functions
  • Integration tests for new endpoints
  • Edge cases covered

5. Documentation:

  • JSDoc comments for complex functions
  • README updates for new features
  • API documentation for new endpoints

Naming Conventions

Variables and functions: camelCase

typescript
const userName = "test";
function getUserById(id: string) {}

Classes and interfaces: PascalCase

typescript
class UserService {}
interface UserData {}

Constants: UPPER_SNAKE_CASE

typescript
const MAX_RETRY_COUNT = 3;
const DEFAULT_TIMEOUT_MS = 5000;

Files: kebab-case

user-service.ts
auth-middleware.ts

Type imports: Prefix with type keyword

typescript
import type { Request, Response } from "express";

Best Practices

1. Prefer Const

typescript
// Prefer const over let
const value = "immutable";

// Use let only when reassignment is needed
let counter = 0;
counter++;

2. Avoid Mutation

typescript
// Bad: Mutation
function addItem(arr: string[], item: string) {
  arr.push(item);
  return arr;
}

// Good: Immutable
function addItem(arr: string[], item: string): string[] {
  return [...arr, item];
}

3. Use Template Literals

typescript
// Bad: String concatenation
const message = "Hello, " + name + "!";

// Good: Template literal
const message = `Hello, ${name}!`;

4. Destructuring

typescript
// Bad: Accessing properties repeatedly
function formatUser(user) {
  return `${user.name} (${user.email})`;
}

// Good: Destructuring
function formatUser({ name, email }: User): string {
  return `${name} (${email})`;
}

5. Optional Chaining

typescript
// Bad: Nested checks
const email = user && user.contact && user.contact.email;

// Good: Optional chaining
const email = user?.contact?.email;

6. Nullish Coalescing

typescript
// Bad: Logical OR (falsy values)
const port = config.port || 3000; // 0 would fallback to 3000

// Good: Nullish coalescing (null/undefined only)
const port = config.port ?? 3000; // 0 is valid

Next Steps

Released under the MIT License.