DEV Community

Rafa Rafael
Rafa Rafael

Posted on

The Frontend Fortress: Building a Bulletproof Code Quality Pipeline

The frontend ecosystem is chaotic. Unlike the server-side where a crash is logged and the server restarts, a frontend crash leaves the user staring at a blank white screen.

You are likely already using Prettier for code style. But Prettier is just makeup; it doesn't stop your application from leaking memory or crashing due to undefined props.

To build a truly stable applicationβ€”whether it is a modern React/Vue SPA or a legacy jQuery monolithβ€”you need to enforce strict types, architectural boundaries, and performance rules automatically.

Here is how to set up a "Frontend Fortress" that catches bugs before they hit the browser.


1. Static Analysis: The First Line of Defense

The Problem: JavaScript is loosely typed. You pass a string where a number is expected, and the app runs fine until a specific user action crashes it.
The Solution: Strict TypeScript & ESLint.

TypeScript (Strict Mode)

TypeScript is your version of "Larastan." But it only works if you turn the safety features on.

Implementation:
Ensure your tsconfig.json has the strictest settings enabled. This forces you to handle null and undefined explicitly.

// tsconfig.json
{
  "compilerOptions": {
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    "noUncheckedIndexedAccess": true // Vital: Prevents accessing array[10] without checking if it exists
  }
}
Enter fullscreen mode Exit fullscreen mode

ESLint (The Logic Police)

ESLint catches bad patterns, like missing dependency arrays in Hooks or accidental global variables in legacy code.

Implementation:
Install strict plugins for your specific stack.

npm install eslint eslint-plugin-vue eslint-plugin-react-hooks eslint-plugin-jquery --save-dev
Enter fullscreen mode Exit fullscreen mode

Configure .eslintrc.json (Multi-Stack Support):

{
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "plugin:vue/vue3-recommended",      // For Vue
    "plugin:react-hooks/recommended"    // For React
  ],
  "rules": {
    "no-var": "error", // For legacy: Kill "var", force const/let
    "eqeqeq": "error", // For legacy: Force === over ==
    "vue/multi-word-component-names": "off",
    "react/jsx-no-bind": "error",
    "no-console": ["warn", { "allow": ["warn", "error"] }]
  },
  "overrides": [
    {
      "files": ["**/*.js", "**/*.jquery.js"],
      "env": { "jquery": true } // Enable jQuery globals only for legacy files
    }
  ]
}
Enter fullscreen mode Exit fullscreen mode

2. Enforce Architecture: Dependency Cruiser

The Problem: "Import Hell." A junior developer imports a heavy API service directly into a UI button, or creates circular dependencies that break the build.
The Solution: Dependency Cruiser.

This works for React, Vue, and Vanilla JS. It validates who is allowed to import whom.

Implementation:

npm install --save-dev dependency-cruiser
Enter fullscreen mode Exit fullscreen mode

Create .dependency-cruiser.js. Here is a rule that enforces a clean flow: Components can use Logic/Stores, but Components cannot call the API directly.

module.exports = {
  forbidden: [
    {
      "name": "no-components-importing-api",
      "comment": "UI components must use Hooks/Composables. Do not call API directly.",
      "from": { "path": "^src/(components|views)" },
      "to": { "path": "^src/api" }
    },
    {
      "name": "no-legacy-mixing",
      "comment": "Modern Vue/React code should not import legacy jQuery scripts.",
      "from": { "path": "^src/(components|features)" },
      "to": { "path": "^src/legacy" }
    },
    {
      "name": "no-circular",
      "severity": "error",
      "from": {},
      "to": { "circular": true }
    }
  ]
};
Enter fullscreen mode Exit fullscreen mode

3. Git Hooks: The Gatekeeper

The Problem: You configure these tools, but developers forget to run them.
The Solution: Husky + Lint Staged.

We run checks only on the files changed in the current commit.

Implementation:

  1. Install:

    npm install husky lint-staged --save-dev
    npx husky init
    
  2. Configure package.json:

    "lint-staged": {
      "**/*.{ts,tsx,vue,js}": [
        "prettier --write",  // Format code
        "eslint --fix"       // Fix logic errors
      ],
      "**/*.{ts,tsx,vue}": [
        "bash -c 'tsc --noEmit'" // Strict type check (add vue-tsc for Vue)
      ]
    }
    
  3. Update .husky/pre-commit:

    npx lint-staged
    

Now, if a developer tries to commit a broken Vue component or a legacy script with syntax errors, the commit is blocked.


4. The Brain: AI Instructions (The Most Important Part)

The Problem: AI assistants defaults to writing "Tutorial Code." It writes Mixins for Vue (deprecated), Class Components for React (outdated), or spaghetti jQuery for legacy.
The Solution: Custom Instructions.

Create a .github/copilot-instructions.md file. This forces Copilot to act like a Senior Architect for your specific stack.

Copy and paste these exact rules into that file:

# Senior Frontend Architect Instructions

You are a Senior Frontend Architect. You prioritize performance, type safety, and maintainability. Determine the technology stack of the current file (React, Vue, or Legacy JS) and apply the specific rules below.

## 🟒 REACT GUIDELINES

### 1. Separation of Concerns
* **No Logic in JSX:** Move complex logic, data fetching, or transformation into Custom Hooks.
* **Structure:** `Component` -> `Custom Hook` -> `API Layer`.

### 2. Performance
* **Derived State:** NEVER use `useEffect` to update state based on props. Use `useMemo` or calculate during render.
* **Referential Integrity:** Do not pass inline objects/functions to memoized components.

### 3. Anti-Patterns
* ❌ No `default export`. Use named exports.
* ❌ No prop drilling (> 2 levels). Use Composition or Context.
* ❌ No `any` type. Define strict interfaces.

---

## πŸ”΅ VUE.JS GUIDELINES (Vue 3+)

### 1. Script Setup & Composition API
* **ALWAYS** use `<script setup lang="ts">`.
* **NEVER** use the Options API (`data`, `methods`, `computed` object syntax).
* **NEVER** use Mixins. Use Composables (`useFeature`) instead.

### 2. Reactivity
* Use `ref` for primitives and `reactive` for distinct objects.
* Destructure props using `const { propName } = toRefs(props)` if you need to retain reactivity in composables.

### 3. State Management
* Avoid "Prop Drilling." Use **Pinia** for global state.
* Avoid "Event Bubbling" up multiple layers. Use `defineEmits` strictly typed.

### 4. Style Guide
* **Multi-word names:** All component files must be multi-word (e.g., `UserCard.vue`, not `Card.vue`).
* **Directives:** Use shorthands (`@click` instead of `v-on:click`, `:src` instead of `v-bind:src`).

---

## 🟠 LEGACY JAVASCRIPT & JQUERY GUIDELINES

### 1. Global Namespace Protection
* **NEVER** declare variables in the global scope.
* Wrap all code in an **IIFE** (Immediately Invoked Function Expression) or use ES Modules if the build system permits.
    ```

javascript
    (function($) {
        // Code goes here
    })(jQuery);


    ```

### 2. DOM Performance (Cache your Selectors)
* **NEVER** query the DOM inside a loop or repeated event.
* **ALWAYS** cache selectors in variables prefixed with `$`.
    * ❌ Bad: `$('.btn').click(function() { $('.content').show(); })`
    * βœ… Good:
        ```

javascript
        const $btn = $('.btn');
        const $content = $('.content');
        $btn.on('click', () => $content.show());


        ```

### 3. Event Delegation
* **NEVER** bind events to elements that don't exist yet (dynamic content).
* **ALWAYS** use event delegation bound to a static parent.
    * βœ… `$(document).on('click', '.dynamic-button', function() { ... })`

### 4. Modernization
* Use `const` and `let`. **NEVER** use `var`.
* Use Template Literals (\`variable: ${x}\`) instead of string concatenation.
* Use `Array.forEach`, `map`, `filter` instead of `for` loops where possible.

---

## πŸ“‚ DIRECTORY STRUCTURE

### Feature-Based (React/Vue)
Group files by "feature" (business domain), not technical type.

src/
β”œβ”€β”€ features/
β”‚   β”œβ”€β”€ auth/
β”‚   β”‚   β”œβ”€β”€ components/  # LoginForm.vue / LoginForm.tsx
β”‚   β”‚   β”œβ”€β”€ hooks/       # useAuth.ts
β”‚   β”‚   β”œβ”€β”€ store/       # authStore.ts (Pinia/Zustand)
β”‚   β”‚   └── api/         # authApi.ts

### Legacy Structure
Separate scripts clearly.

src/
β”œβ”€β”€ legacy/
β”‚   β”œβ”€β”€ modules/         # Isolated functional modules
β”‚   β”‚   β”œβ”€β”€ cart.module.js
β”‚   β”‚   └── ui.module.js
β”‚   └── main.js          # Entry point

## πŸ§ͺ TESTING STANDARDS (Vitest / RTL)

* **Prioritize User Behavior:** Test what the user sees, not internal state.
* **Selectors:** Use `getByRole` (accessibility) > `getByText`. Avoid `querySelector` in tests.
* **Mocking:** Mock network requests (MSW), not business logic functions.
Enter fullscreen mode Exit fullscreen mode

Conclusion

By implementing this pipeline, you stop fighting fires and start building features.

  1. TypeScript/ESLint prevents crashes across all frameworks.
  2. Dependency Cruiser stops spaghetti code in both SPAs and Legacy apps.
  3. Husky enforces the rules before commit.
  4. Copilot is now trained to write modern React, clean Vue, and disciplined Legacy JS.

This is how you scale a frontend codebase without it collapsing under its own weight.

Enjoy!

Top comments (0)