Frontend Quality Gates
See also: Backend Quality Gates
Backend linters catch async footguns. Type checkers prevent runtime explosions. Now the frontend's turn.
JavaScript fails silently. That's the whole problem in one sentence.
You call a function with the wrong arguments. It runs. You access a property that doesn't exist. It runs. You forget to handle null. It runs. Everything runs. Nothing works. Users complain. You have no idea why.
This is fine. Everything is fine.
Just kidding. This is chaos. And like the backend, none of this is even testing yet. We're just checking that the code isn't obviously broken before we bother running actual tests.
The bar is still on the floor. Let's at least step over it.
TypeScript: Because JavaScript Trusts You Too Much
JavaScript has no types. This was a design decision. It was wrong.
function processUser(user) {
return user.name.toUpperCase();
}
What is user? An object? A string? A promise? JavaScript doesn't know. JavaScript doesn't care. JavaScript believes anything is possible.
JavaScript is an optimist. You shouldn't be.
TypeScript strict mode forces you to say what things are:
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Now the compiler yells at you:
// TypeScript rejects this
function processUser(user) { // Error: implicit 'any'
return user.name.toUpperCase();
}
// TypeScript accepts this
function processUser(user: User): string {
return user.name.toUpperCase();
}
Is it annoying? Yes. Does it catch bugs before users do? Also yes. That's the trade.
I use strict: true and noUncheckedIndexedAccess. The second one is particularly annoying. It assumes array access might return undefined. Because it might. And you should handle that.
ESLint: The Nitpicker You Need
TypeScript catches type errors. ESLint catches everything else.
Unused variables. Inconsistent formatting. Dangerous patterns. That console.log you left in production code. ESLint notices. ESLint judges.
I use the Airbnb style guide. It's opinionated, strict, and battle-tested. Thousands of engineers have argued about these rules so I don't have to.
// eslint.config.js
import { configs, extensions, plugins } from 'eslint-config-airbnb-extended';
export default [
plugins.stylistic,
plugins.importX,
plugins.react,
plugins.reactA11y,
plugins.reactHooks,
plugins.typescriptEslint,
...extensions.base.typescript,
...extensions.react.typescript,
...configs.react.recommended,
...configs.react.typescript,
];
One import. Hundreds of rules. TypeScript support, React hooks, accessibility, import ordering. All preconfigured.
no-explicit-any is the important one. It closes the escape hatch. You can't just type any and pretend you have type safety. You either type things properly or the build fails.
Some people find this restrictive. Those people debug production issues at 2 AM. I watch Netflix at 2 AM. Different choices.
Storybook: Because Components Lie
Here's a fun game: refactor a component, run the app, click around, ship it. Three months later, discover you broke the loading state on a page nobody visits often.
Components have many states. Happy path, error state, loading state, empty state, edge cases. You can't manually test all of them every time. You won't. I won't. Nobody will.
Storybook documents every state:
// Button.stories.tsx
export const Primary: Story = {
args: {
variant: 'primary',
children: 'Click me',
},
};
export const Loading: Story = {
args: {
isLoading: true,
children: 'Loading...',
},
};
export const Disabled: Story = {
args: {
disabled: true,
children: 'Nope',
},
};
Run build-storybook in CI. If any component can't render in any state, the build fails. You broke something. Fix it before it ships.
Bonus: AI can read these stories. It understands what states exist. It generates code that handles them. Documentation that actually gets used.
The Gate
One job per check. When something fails, you know exactly what.
.frontend-quality:
stage: quality
image: node:lts-slim
cache:
key: npm-frontend-${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
- ~/.npm
before_script:
- cd frontend
- npm ci --prefer-offline
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
eslint:
extends: .frontend-quality
script:
- npm run lint
typecheck:
extends: .frontend-quality
script:
- npm run typecheck
storybook:build:
extends: .frontend-quality
script:
- npm run build-storybook --quiet
artifacts:
paths:
- frontend/storybook-static
expire_in: 1 week
when: always
Three jobs. Same stage. Run in parallel.
The hidden job — .frontend-quality starts with a dot. GitLab won't run it directly. It's a template.
npm ci — Not npm install. ci is faster, stricter, and uses the lockfile exactly. No surprises.
--prefer-offline — Use cached packages when possible. Network is slow. Cache is fast.
Parallel execution — All three jobs run at the same time. ESLint fails? You see it immediately. TypeScript fails too? You see both. Fix them together.
Storybook artifacts — Keep the built Storybook around. when: always saves it even if the build fails. Useful for debugging why that one component broke.
When one job fails, you see exactly which one in the pipeline view. No scrolling through logs. No guessing.
-
eslintfailed → style issues or dangerous patterns -
typecheckfailed → type errors -
storybook:buildfailed → component can't render
None of this verifies that the code does what it should. That's what tests are for. This just verifies it's not obviously broken.
The bar is low. But you'd be surprised how many projects can't clear it.
Here's the full picture:
stages:
- quality
.frontend-quality:
stage: quality
image: node:lts-slim
cache:
key: npm-frontend-${CI_COMMIT_REF_SLUG}
paths:
- frontend/node_modules
- ~/.npm
before_script:
- cd frontend
- npm ci --prefer-offline
allow_failure: false
rules:
- if: $CI_PIPELINE_SOURCE == "merge_request_event"
- if: $CI_COMMIT_BRANCH == $CI_DEFAULT_BRANCH
eslint:
extends: .frontend-quality
script:
- npm run lint
typecheck:
extends: .frontend-quality
script:
- npm run typecheck
storybook:build:
extends: .frontend-quality
script:
- npm run build-storybook --quiet
artifacts:
paths:
- frontend/storybook-static
expire_in: 1 week
when: always
Copy, paste, adapt. It works.
The Point
Frontend code fails in creative ways. Silent failures. Runtime errors. "It works on my machine" but not in Safari. Components that render fine until you pass them unexpected props.
I can't catch all of this manually. My attention span isn't that good. Nobody's is.
So I automate the obvious stuff:
- Types must be explicit
- Code must follow consistent patterns
- Components must render without crashing
The machines catch what I miss. The pipeline blocks what I'd regret.
Same deal as the backend. Write the rules once. Enforce them forever.
Next up: [Security] -coming soon- — Dependencies are other people's code. And other people make mistakes.
Top comments (0)