Md Mominul Islam | Software and Data Enginnering | SQL Server, .NET, Power BI, Azure Blog

while(!(succeed=try()));

LinkedIn Portfolio Banner

Latest

Home Top Ad

Responsive Ads Here

Tuesday, September 15, 2026

JavaScript for Backend Developers — Part 6: Testing, TypeScript & Code Quality — The Night the Deploy Broke

JavaScript for Backend Developers — Part 6: Testing, TypeScript & Code Quality — The Night the Deploy Broke | FreeLearning365
🚀 JavaScript for Backend Developers
🗺️ The Complete 7-Part Journey
Navigate anywhere in the series
Learning Path Part 1 of 7
🏆 Part 7 → AI + Observability + Production + 3 Capstone Projects
JavaScript for Backend Developers — Part 6: Testing, TypeScript & Code Quality — The Night the Deploy Broke | FreeLearning365
Part 6 of 7 · JavaScript for Backend Developers

The Night the Deploy Broke —
A Story About Testing, Types & Quality.

It was 2:47 AM. The pager went off. Nadia's payment service — 14,000 lines of JavaScript, zero tests — had just charged 47 customers twice. This is her story, and every lesson she learned about testing, TypeScript, and code quality in the 72 hours that followed.

📖 ~85 min deep read 🎭 Story-driven, chapter by chapter ⚡ 100+ code examples 🎮 9 interactive demos

00 · Prologue — 02:47 AM

The Slack notification was one line: "we're seeing double charges on the payment dashboard". Three minutes later: "~50 users affected". Three minutes after that: "customer support inbox is on fire".

Nadia opened her laptop in the dark. She'd joined this startup eight months ago. She'd shipped the payment retry logic herself. It had "worked fine in staging" and "worked fine for two months in production". Tonight, at 02:47 AM, it had decided to not work fine.

The retry loop she'd written was supposed to re-attempt failed charges. But it had a subtle bug: when the upstream bank API returned a 502 gateway timeout — meaning "we don't know if the charge went through" — her code treated it like a failure and retried. Sometimes the charge had gone through. The retry charged the customer again.

The fix, once she found it at 04:19 AM, was three lines of code. But finding it had cost the company $18,000 in refunds, a very awkward all-hands meeting, and Nadia's weekend.

This is the story of what Nadia learned in the 72 hours that followed — and what you can learn now, without the 2 AM pager going off.

If you've written JavaScript for a while, you've probably had a night like Nadia's. Maybe not payment systems, maybe something smaller. But the feeling is universal: "if only I had a test for this".

Part 6 is about those tests. And TypeScript. And every tool in between that turns JavaScript from "it works on my machine" into "it works, forever, for everyone".

Night 1
The Incident. Double charges discovered. Nadia hunts for the bug.
Day 1
Post-Mortem. Leadership asks: "why didn't we catch this?" Answer: no tests.
Day 2
The First Test. Nadia writes one test. It's easier than she feared.
Day 3
The Rewrite. Testing reveals design flaws. TypeScript catches 11 more bugs. ESLint catches 40+.
Day 4
The Resolution. Confident deploy. Nadia sleeps through the night.
🎬

How to read this article: Every chapter is a scene from Nadia's week. Every technical concept arrives just when she needs it. By the end, you'll have learned testing, TypeScript, and code quality the way a senior engineer learns them — in the flow of solving real problems.

01 · The Cost of No Tests

Day 1, 10:00 AM — the post-mortem. Twelve people on a video call. Leadership wanted answers.

"How do we know this won't happen again?" asked the CTO. Nadia opened her mouth to answer, and then realized she didn't have one. She didn't know. She hoped. Hope was not a good enough answer.

The engineering manager spoke up: "We need automated tests before we touch that module again." Nadia nodded. Everyone nodded. Nobody wanted to be the one to say they'd never written a JavaScript test.

If you've never written a test in JavaScript — welcome to a very large club. Most backend developers who "do JavaScript on the side" have skipped this entirely. And the reason is usually one of five excuses:

"I don't have time."

You don't have time now. You'll have far less time at 2 AM when a payment service is failing.

"My code is too simple to test."

Simple code is the easiest to test. If it's too simple to test, it's too simple to break — so write the test in 30 seconds.

"Testing JS is complicated."

It was in 2015. Today, Vitest makes it a 4-line setup. Modern tooling has eliminated 90% of the friction.

"Tests slow down deployment."

Tests protect deployment. Every minute of tests saves hours of incident response.

"I write correct code."

You think you do. Nadia thought she did too. So does every engineer who ships a bug at 2 AM.

"My team doesn't require it."

Your team will require it the day after your first incident. Get ahead of that day.

What a Test Actually Is

A test is a small piece of JavaScript that calls your code and checks what happened. That's the entire concept. The sophistication is in the details — but the core idea fits in one sentence:

🧪

The chemistry lab metaphor:

A test is an experiment. You control the inputs, run the reaction, and check whether the output matches what chemistry says should happen. Over time, you build a library of experiments. Every new batch of chemicals goes through the whole library before it ships. If anything fails, the batch is rejected.

Your code is the chemical. The test suite is the lab procedure. The CI pipeline is the automated lab technician who runs it every single time.

What Tests Actually Buy You

Real, measurable returns — not abstract "quality" platitudes:

Benefit What It Means in Practice
Confidence to refactor You can rewrite internals without fear — the tests will catch any behavioral change.
Living documentation A test file shows exactly what the code is supposed to do. Better than comments.
Faster debugging When a test fails, you know exactly which function broke. No more "it worked yesterday".
Design pressure Untestable code is usually badly designed code. Testing pushes you toward better structure.
Regression prevention Every bug you fix gets a test. That bug can never silently return.
Async sanity Async bugs are the worst to reproduce manually. Tests reproduce them in milliseconds.
Team onboarding New engineers run the tests to understand behavior — no oral tradition required.
Sleep at night You can merge on Friday afternoon without a 2 AM pager.
🎯

The economics that changed everything: A bug that reaches production costs 100× more to fix than one caught in a test. A bug that reaches a customer costs 1000×. A bug that charges a customer twice costs $18,000 plus a very awkward meeting. Tests are the cheapest insurance you can buy in software.

02 · The Test Pyramid — Simplified

Day 1, 2:00 PM — Nadia's desk. She pulled up a note she'd saved months ago: "the test pyramid". She had no idea what it meant at the time. Now she needed to.

Her mentor, Anwar, had sent her a Slack message: "Don't start with end-to-end tests. Start with the bottom. You'll get 10× the value for 10% of the effort."

The "test pyramid" is a heuristic (originally from Mike Cohn) that tells you roughly how many of each kind of test to write. The metaphor is simple: many small fast tests at the base, fewer medium tests in the middle, a handful of expensive end-to-end tests at the top.

🎭 End-to-End (E2E) — ~5–10% Slow, brittle, but real. Run in CI before deploy.
🧩 Integration — ~20–30% Medium speed. Verify modules work together.
⚡ Unit — ~60–75% Fast, focused. Verify one function at a time.

What Each Layer Tests

Layer Tests Speed Example
Unit One function or one small module, in isolation Milliseconds calculateTax(order) returns correct value
Integration Several modules together — service + DB, service + external API Hundreds of ms The order endpoint writes to DB and returns the right JSON
E2E The entire system, as a user would experience it Seconds to minutes Login → add to cart → pay → see confirmation page
🏗️

Why the shape matters:

Unit tests are cheap to write, fast to run, and easy to make reliable. E2E tests are the opposite — slow, flaky, expensive. If you invert the pyramid (many E2E, few unit), your CI takes 45 minutes and fails randomly. If you get it right, CI runs in 2 minutes and you trust every result.

What Nadia Decided

She didn't need to write all three layers. She needed to write the right tests for her payment retry logic. Specifically:

  • Unit: "given a 502 error, don't retry". This was the fix.
  • Unit: "given a network timeout, retry once". This was the original behavior.
  • Unit: "given a 4xx client error, don't retry". Sanity check.
  • Integration: "the whole retry mechanism, given a mocked bank API, produces the right number of calls".

Four tests. Not forty. This is the trick to testing: you don't test everything. You test the things that break.

03 · Writing Your First Test (Vitest)

Day 2, 9:14 AM. Nadia opened a terminal in her project. She typed npm install -D vitest, hit enter, and held her breath.

Twelve seconds later, it was installed. She created a file called retry.test.js next to retry.js, wrote four lines of test code, and ran npx vitest.

"✓ 4 passed (12ms)" appeared in green.

"Wait, that's it?" she whispered. The whole process — install, write, run — had taken under two minutes. She had put it off for months.

Why Vitest in 2026?

For years, Jest was the default. It's still excellent. But Vitest — built on top of Vite — has become the modern choice for a few concrete reasons:

Native ESM support

Zero config for ES modules. Jest needs transforms. Vitest just works.

🔥

Blazing fast

Reuses Vite's transform pipeline. Often 5–10× faster than Jest for large suites.

🎯

Jest-compatible API

If you know Jest, you already know Vitest. describe, it, expect — identical.

🎨

Beautiful UI

Built-in UI mode with DOM inspector, live reload, and coverage visualization.

🔄

Watch mode by default

Only re-runs tests affected by your changes. Save → tests run in <100ms.

🧪

Full TypeScript

Type-checked tests out of the box, no ts-jest configuration needed.

But if you're already using Jest, don't rush to switch. Both are excellent. The concepts below work in both.

Setup — Literally Three Steps

setup.sh Shell
# 1. Install Vitest
npm install -D vitest

# 2. (Optional) Add a test script to package.json
# "scripts": { "test": "vitest", "test:run": "vitest run" }

# 3. Run it
npx vitest

Vitest auto-discovers files matching *.test.js, *.spec.js, or anything under a __tests__/ folder. No config file needed for the basic case.

The Anatomy of a Test

retry.test.js — Nadia's first test JavaScript
import { describe, it, expect } from 'vitest';
import { shouldRetry } from './retry.js';

describe('shouldRetry', () => {
  it('does NOT retry on HTTP 502 (ambiguous — charge may have succeeded)', () => {
    // Arrange: set up the input
    const error = { status: 502, message: 'Bad Gateway' };

    // Act: call the function
    const result = shouldRetry(error);

    // Assert: check the outcome
    expect(result).toBe(false);
  });

  it('DOES retry on network timeout (safe — no charge was made)', () => {
    const error = { code: 'ETIMEDOUT' };
    expect(shouldRetry(error)).toBe(true);
  });

  it('does NOT retry on 4xx client errors (request was wrong)', () => {
    const error = { status: 400 };
    expect(shouldRetry(error)).toBe(false);
  });
});

Notice the three-part rhythm of every good test:

  1. Arrange — set up the input. (Nadia creates the error object.)
  2. Act — call the function under test. (Nadia calls shouldRetry.)
  3. Assert — check the outcome. (Nadia checks it returns the expected boolean.)

This is often called the AAA pattern. It reads naturally, it's easy to review, and every test framework in the world speaks this shape.

Interactive: Watch Vitest Run in Real Time

Interactive — A Live Test Run

The Most Common Matchers You'll Actually Use

matchers.js — 15 you'll use 95% of the time JavaScript
// Equality
expect(value).toBe(42);                  // Object.is (strict equality)
expect(value).toEqual({ a: 1 });          // deep structural equality
expect(value).toStrictEqual({ a: 1 });    // strict — also checks undefined props

// Truthiness
expect(value).toBeTruthy();                   // any truthy value
expect(value).toBeFalsy();                    // any falsy value
expect(value).toBeNull();                     // === null
expect(value).toBeUndefined();                // === undefined
expect(value).toBeDefined();                  // !== undefined

// Numbers
expect(value).toBeGreaterThan(5);
expect(value).toBeLessThanOrEqual(10);
expect(value).toBeCloseTo(3.14, 2);          // floating point — critical!

// Strings & arrays
expect(str).toContain('hello');
expect(str).toMatch(/^\d{4}-\d{2}-\d{2}$/);     // regex
expect(arr).toHaveLength(3);
expect(arr).toContainEqual({ id: 1 });      // deep containment

// Objects
expect(obj).toHaveProperty('user.name', 'Alice');
expect(obj).toMatchObject({ user: { name: 'Alice' } });  // partial match

// Errors
expect(() => riskyCall()).toThrow();
expect(() => riskyCall()).toThrow('specific message');
expect(() => riskyCall()).toThrow(/regex pattern/);
expect(() => riskyCall()).toThrowError(CustomError);

// Async — the ones that trip people up
await expect(asyncFn()).resolves.toBe('ok');
await expect(asyncFn()).rejects.toThrow('oops');
⚠️

The #1 async testing mistake: forgetting to await the assertion. If you write expect(asyncFn()).resolves.toBe('ok') without await, the test always passes — even if the assertion would fail. Vitest will warn you, but only if you know what to look for.

Testing Async Code — The Two Patterns

async-tests.js JavaScript
// Pattern 1: async/await (recommended — most readable)
it('fetches user by id', async () => {
  const user = await fetchUser(42);
  expect(user.name).toBe('Alice');
});

// Pattern 2: resolves/rejects matchers (concise)
it('fetches user by id', async () => {
  await expect(fetchUser(42)).resolves.toMatchObject({ name: 'Alice' });
});

// Testing error cases
it('throws on missing user', async () => {
  await expect(fetchUser(999)).rejects.toThrow('User not found');
});

// ❌ The classic mistake (silently passes on failure!)
it('BROKEN TEST', () => {
  expect(fetchUser(42)).resolves.toBe('wrong');  // no await!
});
// This test always "passes" because the assertion promise isn't awaited.
// Vitest will warn about this — always read the warnings.

04 · Test Doubles: Mocks, Spies, Stubs, Fakes

Day 2, 11:30 AM. Nadia's second test needed to call the real bank API. Obviously that was a bad idea — she couldn't charge real customers in a test.

Anwar sent her a message: "You need test doubles. Think of them as stunt doubles for your dependencies. The real bank API is the star; the test double is the person who does the dangerous scenes without anyone getting hurt."

Test doubles are substitutes for the real dependencies your code uses — databases, HTTP clients, clocks, file systems. The term comes from the movie industry: a stunt double does the dangerous scenes so the real actor doesn't have to. Same idea.

The Four Kinds of Test Doubles

Type What It Does When to Use
Stub Returns a fixed value "Pretend the DB returns this user"
Spy Records how it was called "Did we call the logger?"
Mock Programmed with expectations "The DB must be called exactly once"
Fake Working simplified implementation In-memory database for tests
🎬

The stunt-double metaphor:

Stub — a trained extra who says the same line every time. You know exactly what they'll do.
Spy — a witness who takes notes on everything that happens.
Mock — a director who has written a script and expects you to follow it exactly.
Fake — a junior actor who can actually act, just not as well as the star.

Nadia's Test — With Doubles

payment.test.js JavaScript
import { describe, it, expect, vi, beforeEach } from 'vitest';
import { chargeCustomer } from './payment.js';

describe('chargeCustomer', () => {
  let mockBank;

  beforeEach(() => {
    // Fresh mock for each test — prevents state leaking between tests
    mockBank = {
      createCharge: vi.fn()  // vi.fn() creates a spy/stub combo
    };
  });

  it('calls the bank API once with the correct amount', async () => {
    // ARRANGE: stub the response
    mockBank.createCharge.mockResolvedValue({ id: 'ch_123', status: 'succeeded' });

    // ACT
    const result = await chargeCustomer(mockBank, {
      customerId: 'cus_abc',
      amountCents: 5000
    });

    // ASSERT: check the outcome
    expect(result.status).toBe('succeeded');

    // ASSERT: verify how the dependency was called (spy behavior)
    expect(mockBank.createCharge).toHaveBeenCalledTimes(1);
    expect(mockBank.createCharge).toHaveBeenCalledWith({
      customerId: 'cus_abc',
      amount: 5000,
      currency: 'usd'
    });
  });

  it('DOES NOT retry on 502 (the fix for the incident!)', async () => {
    // ARRANGE: bank fails with 502 — ambiguous outcome
    mockBank.createCharge.mockRejectedValue(
      Object.assign(new Error('Bad Gateway'), { status: 502 })
    );

    // ACT + ASSERT
    await expect(
      chargeCustomer(mockBank, { customerId: 'cus_abc', amountCents: 5000 })
    ).rejects.toThrow();

    // 🎯 THE CRITICAL ASSERTION — this is what the incident taught her
    expect(mockBank.createCharge).toHaveBeenCalledTimes(1);
    // Not 2. Not 3. Exactly ONE call.
    // If the bug returns, this test fails immediately.
  });

  it('DOES retry on timeout (safe to retry)', async () => {
    // First call times out, second call succeeds
    mockBank.createCharge
      .mockRejectedValueOnce(Object.assign(new Error('timeout'), { code: 'ETIMEDOUT' }))
      .mockResolvedValueOnce({ id: 'ch_124', status: 'succeeded' });

    const result = await chargeCustomer(mockBank, {
      customerId: 'cus_abc',
      amountCents: 5000
    });

    expect(result.status).toBe('succeeded');
    expect(mockBank.createCharge).toHaveBeenCalledTimes(2);
  });
});

The Spy API You'll Use 90% of the Time

spy-api.js JavaScript
const spy = vi.fn();

// Stub the return value
spy.mockReturnValue(42);
spy.mockResolvedValue({ id: 1 });
spy.mockRejectedValue(new Error('oops'));

// Per-call stubs — different return value each call
spy.mockReturnValueOnce('first').mockReturnValueOnce('second');

// Custom implementation
spy.mockImplementation((x) => x * 2);

// Assertions
expect(spy).toHaveBeenCalled();
expect(spy).toHaveBeenCalledTimes(3);
expect(spy).toHaveBeenCalledWith('arg1', 'arg2');
expect(spy).toHaveBeenLastCalledWith('arg3');
expect(spy).toHaveBeenNthCalledWith(2, 'arg2');

// Inspect calls
spy.mock.calls;         // array of argument arrays
spy.mock.calls[0];      // first call's arguments
spy.mock.results;       // array of return values

// Reset between tests
spy.mockClear();      // clears call history, keeps impl
spy.mockReset();      // clears history + impl
spy.mockRestore();    // restores original (for spies on real functions)

Spying on Real Objects (Without Rewriting)

spy-on-real.js JavaScript
import * as logger from './logger.js';

// Wrap an existing method — keeps original behavior but records calls
const spy = vi.spyOn(logger, 'error');

someFunctionThatShouldLog();

expect(spy).toHaveBeenCalledWith('Something went wrong');

// Override the behavior if needed
spy.mockImplementation(() => {});  // silence during test

// ALWAYS restore — critical for tests that share modules
spy.mockRestore();

// Or use automatic cleanup in beforeEach/afterEach:
afterEach(() => {
  vi.restoreAllMocks();
});

Interactive: Spies in Action

Interactive — Watch a Spy Record Calls
🎯

The rule that saves hours of debugging: always restore or reset spies between tests. A leaked spy from test A can make test B pass or fail for mysterious reasons. Use afterEach(() => vi.restoreAllMocks()) as a default habit.

05 · Coverage — The Truth Nobody Tells You

Day 2, 4:00 PM. Anwar sent Nadia a Slack message: "Did you add coverage reporting?" Nadia replied: "Not yet. What percentage should I aim for?"

Anwar's reply was three lines: "80% is a good target, but it's not the goal. The goal is: would your test suite catch the bug you just fixed? If yes, the number is irrelevant. If no, the number is a lie."

Coverage measures what percentage of your code was executed while your tests ran. There are four kinds, and they mean very different things.

Type Measures Usefulness
Line coverage What % of lines ran at least once Basic — most common
Statement coverage What % of statements ran Similar to line coverage
Branch coverage What % of if/else/switch paths ran More meaningful — catches missed edge cases
Function coverage What % of functions were called Rough — can be misleading

Run coverage in Vitest with npx vitest run --coverage. You'll get a report like this:

coverage-output.txt Output
 % Coverage report from v8
-----------------------------------|---------|----------|---------|---------|
File                               | % Stmts | % Branch | % Funcs | % Lines |
-----------------------------------|---------|----------|---------|---------|
All files                          |   84.21 |    72.50 |   88.00 |   84.21 |
 retry.js                          |   95.00 |    90.00 |  100.00 |   95.00 |
 payment.js                        |   78.00 |    65.00 |   80.00 |   78.00 |
-----------------------------------|---------|----------|---------|---------|

The Truth About Coverage Numbers

🚨

100% coverage does NOT mean 100% correct. A test suite can execute every line of your code and still miss every bug. Coverage measures execution, not correctness. Here's a classic example:

coverage-lie.js JavaScript
// The function under test
function divide(a, b) {
  return a / b;
}

// A test with 100% line coverage that proves NOTHING
it('divides two numbers', () => {
  divide(10, 2);   // calls the function → 100% coverage
  // But no assertion! This test passes even if divide() returns garbage.
});

// A test with proper assertions AND edge cases
it('divides two numbers', () => {
  expect(divide(10, 2)).toBe(5);
});

it('handles division by zero', () => {
  // Behavior you actually care about — Infinity? Throw? Null?
  // The test forces you to DECIDE and DOCUMENT the answer.
  expect(divide(10, 0)).toBe(Infinity);
});
🎯

The coverage metaphor:

Coverage is like a security camera that shows whether security guards walked through every hallway. It doesn't tell you if they actually looked at anything. A guard sprinting through the building at 3 AM counts as "100% coverage" — but is a terrible security system.

The right target is: high coverage of critical paths, not 100% of all lines. Payment logic? Aim for 100% branch coverage. UI formatting? 40% is fine.

Sensible Coverage Targets

Code Type Suggested Target Reasoning
Payment / money logic100% branchOne bug = real financial loss
Authentication / authorization95%+ branchSecurity critical
Business rules / state machines90%+ branchComplex logic, many edge cases
Data transformations80%+ branchHigh leverage for unit tests
API controllers70%+ lineMost behavior lives in services
UI formatting / displayNo targetTested by visual regression / E2E
Config filesNoneTesting config is testing the framework

Interactive: Branch Coverage Visualizer

Interactive — See Which Branches Your Tests Cover

06 · Integration & E2E — Testing the Real Thing

Day 2, 7:00 PM. Nadia's unit tests were passing. But she had a nagging feeling. The unit tests proved that shouldRetry returned the right boolean. They didn't prove that the whole payment flow actually worked end-to-end.

Anwar, ever the mentor, sent one more message: "Time for integration tests. Spin up the service, mock the bank API, hit the endpoint, and check the DB. This is where the real confidence comes from."

Integration Tests — The Sweet Spot

Integration tests sit between unit and end-to-end. They spin up your service, connect it to a test database (or in-memory fake), and hit it through its real API. The only thing faked is external services.

payment.integration.test.js JavaScript
import { describe, it, expect, beforeAll, afterAll } from 'vitest';
import { createServer } from './server.js';
import { createTestDb } from './test-helpers.js';

describe('POST /charges endpoint', () => {
  let server, db, baseUrl;

  beforeAll(async () => {
    db = await createTestDb();  // in-memory SQLite or Postgres test schema
    server = await createServer({
      db,
      bankClient: makeFakeBankClient()  // controlled in-memory fake
    });
    baseUrl = `http://localhost:${server.port}`;
  });

  afterAll(async () => {
    await server.close();
    await db.destroy();
  });

  it('creates a charge and persists it to the DB', async () => {
    const res = await fetch(`${baseUrl}/charges`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ customerId: 'cus_test', amountCents: 5000 })
    });

    expect(res.status).toBe(201);
    const charge = await res.json();
    expect(charge.status).toBe('succeeded');

    // Verify it was ACTUALLY persisted — not just returned
    const stored = await db.query('SELECT * FROM charges WHERE id = ?', [charge.id]);
    expect(stored.rows).toHaveLength(1);
    expect(stored.rows[0].amount_cents).toBe(5000);
  });

  it('regression: does NOT double-charge on 502 from bank', async () => {
    // Arrange: bank will fail with 502 on first call
    await db.query('UPDATE bank_client_config SET fail_next = $1', ['502']);

    const res = await fetch(`${baseUrl}/charges`, {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ customerId: 'cus_test2', amountCents: 3000 })
    });

    expect(res.status).toBe(502);

    // THE KEY ASSERTION — only ONE charge row exists for this customer
    const charges = await db.query(
      'SELECT * FROM charges WHERE customer_id = $1',
      ['cus_test2']
    );
    expect(charges.rows).toHaveLength(1);
    // If the incident bug returns, this test fails. Forever. In CI.
  });
});

End-to-End with Playwright — The Final Gate

E2E tests are the most expensive and the most valuable. They spin up the entire system (or hit staging), open a real browser, and simulate a real user.

checkout.spec.js — Playwright E2E JavaScript
import { test, expect } from '@playwright/test';

test.describe('checkout flow', () => {
  test('customer can complete a purchase', async ({ page }) => {
    // 1. Login
    await page.goto('/login');
    await page.fill('[name=email]', 'test@example.com');
    await page.fill('[name=password]', 'testpass123');
    await page.click('button[type=submit]');

    // 2. Add product to cart
    await expect(page).toHaveURL('/dashboard');
    await page.click('[data-testid=add-to-cart]');

    // 3. Go to checkout
    await page.click('[data-testid=cart-icon]');
    await page.click('[data-testid=checkout-button]');

    // 4. Fill payment
    await page.fill('[name=cardNumber]', '4242424242424242');
    await page.fill('[name=cardExpiry]', '12/30');
    await page.fill('[name=cardCvc]', '123');

    // 5. Confirm
    await page.click('[data-testid=confirm-order]');

    // 6. Assert success
    await expect(page.locator('[data-testid=order-confirmation]')).toBeVisible();
    await expect(page.locator('[data-testid=order-status]')).toHaveText('Confirmed');
  });

  test('regression: shows error on bank 502 (no double charge)', async ({ page }) => {
    // Force the fake bank to return 502 on next charge
    await page.request.post('/api/test/force-bank-response', { data: { status: 502 } });

    // ...login, add to cart, checkout...

    await expect(page.locator('[data-testid=error-message]')).toContainText('Payment could not be confirmed');

    // Verify only one charge exists
    const res = await page.request.get('/api/test/charges?customer=test@example.com');
    const charges = (await res.json()).charges;
    expect(charges).toHaveLength(1);
  });
});
🎯

Playwright vs Cypress vs Selenium:

Playwright — modern, fast, great DX, auto-waits, works with Chromium/Firefox/WebKit. The 2026 default.
Cypress — still excellent, especially for component testing. Slightly slower and less flexible.
Selenium — legacy but universal. Use if you have infrastructure reasons.

Playwright is what Anwar recommended to Nadia, and it's what most new projects choose today.

07 · TypeScript — Types as Documentation

Day 3, 9:00 AM. Anwar sat down next to Nadia. "Look at this bug you fixed," he said, pointing at the screen. "Here — the error object. You check error.status for 502. But the bank sometimes returns error.statusCode, right?"

Nadia froze. She hadn't thought of that. If the bank had returned statusCode: 502 instead of status: 502, her code would have retried — and double-charged again. The bug wasn't fully fixed.

"This," Anwar said, "is why we use TypeScript. With types, the compiler would have told you. Without types, only a customer tells you."

Why TypeScript Matters for Backend Developers

You've already used static types in Java, C#, or Go. You know the value. TypeScript gives JavaScript the same safety net — but with a design philosophy that's opt-in and zero-runtime (types disappear at compile time).

🛡️

Catches bugs at compile time

Typos, missing fields, wrong argument types — all found before you run code.

📖

Documentation that can't lie

Types describe what a function expects and returns. They can't drift out of sync.

🔍

Autocomplete that's actually useful

Your editor knows the shape of every object. Refactoring becomes safe.

🤝

Team communication

"What does this function take?" — just look at the signature. No Slack archaeology.

🚀

Zero runtime cost

Types vanish at build time. Your shipped JavaScript is identical to hand-written JS.

🔄

Incremental adoption

You can add TypeScript to one file at a time. No big-bang rewrite required.

The 5-Minute TypeScript Tour

typescript-basics.ts TypeScript
// Primitives — same as JavaScript + type annotations
let name: string = 'Alice';
let age: number = 30;
let isActive: boolean = true;

// Arrays — two syntaxes, same meaning
const tags: string[] = ['admin', 'user'];
const ids: Array<number> = [1, 2, 3];

// Objects — define the shape once, reuse everywhere
interface User {
  id: string;
  email: string;
  name: string;
  age?: number;               // optional — may be undefined
  readonly createdAt: Date;      // cannot be reassigned
}

function greet(user: User): string {
  return `Hello, ${user.name}`;
}

// Now TypeScript catches errors for you
greet({ id: '1', email: 'a@b.com', name: 'Alice' });  // ✓
// greet({ id: '1' });                                     // ✗ missing fields
// greet('Alice');                                         // ✗ wrong type

// Unions — one of several types
type Status = 'pending' | 'paid' | 'shipped' | 'cancelled';

function statusColor(status: Status): string {
  switch (status) {
    case 'pending': return 'yellow';
    case 'paid': return 'green';
    case 'shipped': return 'blue';
    case 'cancelled': return 'red';
  }
}

// Generics — types as parameters
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

const firstUser = first(users);    // firstUser: User | undefined
const firstId = first(ids);        // firstId: number | undefined

Nadia's Bug, Caught by TypeScript

// Without TypeScript — the retry function function shouldRetry(error) { // 🐛 BUG: bank sometimes returns statusCode, sometimes status if (error.status === 502) return false; if (error.code === 'ETIMEDOUT') return true; return false; } // If bank returns { statusCode: 502 }, error.status is undefined // The function falls through to "return false" — correct here, but // if the fallthrough had been "return true", we'd have double-charged. // No warning. No error. Just a silent 2 AM incident.
// With TypeScript — the same code, but SAFE type BankError = | { kind: 'http'; status: number } | { kind: 'network'; code: string } | { kind: 'timeout' }; function shouldRetry(error: BankError): boolean { switch (error.kind) { case 'http': // TS now knows `error.status` exists if (error.status === 502) return false; if (error.status >= 500) return true; return false; case 'network': return true; case 'timeout': return true; } }
// The compiler now catches 3 classes of bugs // 1. Accessing a non-existent property on a narrowed type // if (error.kind === 'http') { error.code } // ✗ Property 'code' does not exist // 2. Missing case in a discriminated union // If you add { kind: 'dns' } later, TS errors here until you handle it. // 3. Wrong types passed in // shouldRetry({ kind: 'http', status: '502' }) // ✗ string is not number // This is the "make illegal states unrepresentable" principle. // If the bug can't compile, it can't run in production.
🎯

The most powerful TypeScript pattern for backend developers: discriminated unions. Instead of a bag of optional fields ({ status?, statusCode?, code?, message? }), model your domain as a union of distinct shapes. Then the compiler forces you to handle every case. This is how you turn "sometimes returns X, sometimes Y" into "the compiler proves I handled both".

08 · TypeScript Patterns That Pay Off

Basic type annotations are just the start. These are the patterns that change how you think about code.

Pattern 1 — Discriminated Unions (You Just Saw This)

Model states as distinct variants, each with a "kind" or "type" tag. Perfect for API responses, form states, and state machines.

discriminated-union.ts TypeScript
// ❌ Before: a bag of optional fields — impossible to reason about
interface ApiResult {
  data?: User;
  error?: Error;
  loading?: boolean;
}
// Is data defined when error is defined? TS doesn't know.

// ✅ After: a discriminated union — mathematically precise
type ApiResult<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

// Now the compiler enforces completeness
function render(result: ApiResult<User>) {
  switch (result.status) {
    case 'idle': return 'Ready';
    case 'loading': return 'Loading...';
    case 'success': return result.data.name;      // ✓ data exists here
    case 'error': return result.error.message;  // ✓ error exists here
  }
  // No default needed — TS knows we covered all cases
}

Pattern 2 — Utility Types (Free Wins)

utility-types.ts TypeScript
interface User {
  id: string;
  email: string;
  name: string;
  createdAt: Date;
  passwordHash: string;
}

// Partial — all fields become optional
type UserUpdate = Partial<User>;
// { id?: string; email?: string; name?: string; ... }

// Required — all fields become required
type CompleteUser = Required<User>;

// Pick — subset of fields
type UserPreview = Pick<User, 'id' | 'name'>;
// { id: string; name: string }

// Omit — everything except these fields (great for API responses)
type PublicUser = Omit<User, 'passwordHash'>;
// { id, email, name, createdAt } — no passwordHash

// Record — object with typed keys and values
type UserById = Record<string, User>;

// Readonly — immutable version
type FrozenUser = Readonly<User>;

// Combining them — the classic API pattern
type CreateUserInput = Omit<User, 'id' | 'createdAt'>;
type UpdateUserInput = Partial<CreateUserInput>;
type UserResponse = Omit<User, 'passwordHash'>;

Pattern 3 — The Result Type (Error Handling That Scales)

result-type.ts TypeScript
// A Result type forces callers to handle both paths
type Result<T, E = Error> =
  | { ok: true; value: T }
  | { ok: false; error: E };

async function fetchUser(id: string): Promise<Result<User>> {
  try {
    const res = await fetch(`/users/${id}`);
    if (!res.ok) {
      return { ok: false, error: new Error(`HTTP ${res.status}`) };
    }
    return { ok: true, value: await res.json() };
  } catch (err) {
    return { ok: false, error: err as Error };
  }
}

// Caller is FORCED to handle both cases
const result = await fetchUser('42');

if (result.ok) {
  console.log('Got user:', result.value.name);
} else {
  console.error('Failed:', result.error.message);
}
// If you forget one branch, TS errors at compile time.
// This is how you make "handle the error" non-optional.
🎯

Why Result types matter: In JavaScript, any function can throw, and any caller can forget to catch. Result types move "must handle the error" from a convention to a compiler-enforced rule. It's how Go does it, and it's how you get the same rigor in TypeScript.

Pattern 4 — Zod: Runtime Validation + Type Inference

TypeScript types disappear at runtime. So how do you validate that a JSON payload from an untrusted source matches your types? Zod (or Yup, Valibot, ArkType) gives you runtime validation and static types from a single declaration.

zod.ts TypeScript
import { z } from 'zod';

// Define the schema ONCE — get runtime validation AND a TS type
const CreateUserSchema = z.object({
  email: z.string().email(),
  name: z.string().min(1).max(100),
  age: z.number().int().min(18).optional(),
  role: z.enum(['user', 'admin']).default('user')
});

// Extract the TypeScript type from the schema
type CreateUserInput = z.infer<typeof CreateUserSchema>;
// { email: string; name: string; age?: number; role: 'user' | 'admin' }

// Use in an Express controller
app.post('/users', (req, res) => {
  const parsed = CreateUserSchema.safeParse(req.body);
  if (!parsed.success) {
    return res.status(400).json({ errors: parsed.error.issues });
  }

  // parsed.data is now typed as CreateUserInput — safe to use
  const user = await userService.create(parsed.data);
  res.status(201).json(user);
});
🎯

Why this pattern is transformative: validate once at the boundary of your system (HTTP handlers, queue consumers), and every downstream function can trust the type. No more "is this really a number?" checks scattered through your business logic.

09 · JSDoc — Type Safety Without a Build Step

Day 3, 2:00 PM. Nadia wanted to use TypeScript, but the team had pushed back: "our build pipeline isn't ready" / "we can't rewrite 15,000 lines" / "our CI will break". Anwar smiled. "You don't need to rewrite anything. Try JSDoc first."

JSDoc is comment-based type annotation. You write types in /** */ comments, and TypeScript — configured with checkJs: true — type-checks your existing JavaScript without a build step. It's the perfect middle ground for teams that aren't ready for a full TypeScript migration.

jsdoc.js — types in plain JavaScript JavaScript
/**
 * Charge a customer via the bank API.
 *
 * @param {Object} options - Charge options
 * @param {string} options.customerId - Customer identifier
 * @param {number} options.amountCents - Amount in cents (integer)
 * @param {string} [options.currency='usd'] - ISO 4217 currency code
 * @returns {Promise<{ id: string, status: string, amount: number }>}
 * @throws {Error} If the customer ID is invalid
 *
 * @example
 * const charge = await chargeCustomer({
 *   customerId: 'cus_abc',
 *   amountCents: 5000
 * });
 */
export async function chargeCustomer({
  customerId,
  amountCents,
  currency = 'usd'
}) {
  if (!customerId) throw new Error('customerId required');
  return bankApi.createCharge({ customerId, amount: amountCents, currency });
}

// Define reusable types
/**
 * @typedef {Object} User
 * @property {string} id
 * @property {string} email
 * @property {string} name
 * @property {Date} createdAt
 */

// Discriminated unions work too
/**
 * @typedef {Object} HttpError
 * @property {'http'} kind
 * @property {number} status
 */
/**
 * @typedef {Object} NetworkError
 * @property {'network'} kind
 * @property {string} code
 */
/**
 * @typedef {HttpError | NetworkError} BankError
 */

/**
 * @param {BankError} error
 * @returns {boolean}
 */
function shouldRetry(error) {
  if (error.kind === 'http') {
    return error.status === 502 ? false : error.status >= 500;
  }
  if (error.kind === 'network') {
    return error.code === 'ETIMEDOUT';
  }
  return false;
}

Setting It Up — 3 Steps, 5 Minutes

jsconfig.json — enable type checking JSON
{
  "compilerOptions": {
    "checkJs": true,
    "allowJs": true,
    "noEmit": true,
    "strict": true,
    "target": "es2022",
    "module": "node16",
    "moduleResolution": "node16"
  },
  "include": ["src/**/*.js", "src/**/*.test.js"],
  "exclude": ["node_modules"]
}
🎯

The migration path Nadia chose:

Week 1: Add jsconfig.json, get editor IntelliSense for free.
Week 2: Add // @ts-check at the top of one file. Fix the errors it reveals.
Week 3–4: Convert that file to .ts. Repeat one file at a time.
Month 2: Enable strict: true and see the real bugs the compiler was hiding.

The team never had to "pause for a rewrite". Each step delivered value on its own.

10 · ESLint, Prettier & Biome

Day 3, 5:00 PM. Nadia's code review had 47 comments, 40 of which were style nits: missing semicolons, inconsistent quotes, indentation. "This is a waste of everyone's time," she thought.

Anwar's last message of the day: "Install ESLint and Prettier tonight. Tomorrow, your team will argue about architecture instead of semicolons."

The Division of Labor

Tool Responsibility Example
Prettier (formatter) Whitespace, quotes, semicolons, line length Converts 'hi' to "hi" (or vice versa)
ESLint (linter) Code quality, potential bugs, style rules Warns on unused variables, missing await, == vs ===
Biome (all-in-one) Both — formatter + linter, single tool Rust-based, 20× faster than ESLint+Prettier

For a new project in 2026, Biome is arguably the best choice — one tool, one config, one command, blazing speed. But if you're on an existing project with an ESLint setup, don't migrate unless you have a reason.

Prettier — Zero Decisions

.prettierrc.json JSON
{
  "semi": true,
  "singleQuote": true,
  "trailingComma": "all",
  "printWidth": 100,
  "tabWidth": 2,
  "arrowParens": "always"
}

Prettier takes a strong stance: no configuration debates. It reformats your entire codebase in one consistent style. Every PR becomes smaller. Reviews focus on logic, not whitespace.

prettier-demo.js — Before & After JavaScript
// ❌ Before — inconsistent
const users=fetchUsers( { page:1,size:20 })
if(users.length>0){console.log('found')}

// ✅ After Prettier
const users = fetchUsers({ page: 1, size: 20 });
if (users.length > 0) {
  console.log('found');
}

ESLint — Rules That Catch Real Bugs

eslint.config.js — modern flat config JavaScript
import js from '@eslint/js';
import tseslint from 'typescript-eslint';

export default tseslint.config(
  js.configs.recommended,
  ...tseslint.configs.recommended,
  {
    rules: {
      // Catch real bugs
      'no-unused-vars': 'error',
      'no-undef': 'error',
      'no-console': ['warn', { allow: ['error', 'warn'] }],
      'eqeqeq': ['error', 'always'],           // === over ==
      'no-floating-promises': 'error',    // await or .catch() every promise
      'no-misused-promises': 'error',
      'require-await': 'warn',

      // Modern syntax
      'prefer-const': 'error',
      'no-var': 'error',
      'prefer-template': 'warn',
      'prefer-arrow-callback': 'warn',

      // Security
      'no-eval': 'error',
      'no-implied-eval': 'error',
      'no-new-func': 'error'
    }
  }
);
🎯

The rule that would have caught Nadia's bug: no-floating-promises. Her retry logic had a promise that wasn't properly awaited — a floating promise. The error happened later, in a place with no context. With this rule enabled, ESLint would have flagged it at code review.

Biome — The All-in-One Alternative

biome.json — one config, one tool JSON
{
  "$schema": "https://biomejs.dev/schemas/1.9.0/schema.json",
  "formatter": {
    "enabled": true,
    "indentStyle": "space",
    "indentWidth": 2,
    "lineWidth": 100
  },
  "linter": {
    "enabled": true,
    "rules": {
      "recommended": true,
      "suspicious": {
        "noExplicitAny": "warn"
      }
    }
  }
}

Install once, run npx biome check --write . — it formats and lints in a fraction of the time ESLint + Prettier would take. For a 50,000-line codebase: ~2 seconds vs. ~15 seconds.

Integrating with Your Editor

The setup that makes these tools invisible:

1. Install the Prettier (or Biome) extension for VS Code.
2. Enable "editor.formatOnSave": true in settings.
3. Install the ESLint extension and enable "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit" }.
4. Add a pre-commit hook with husky + lint-staged so nobody commits unformatted code.

After this, you literally cannot write inconsistent code — the editor fixes it before you think about it.

11 · AI That Writes Good Tests

Day 3, 8:00 PM. Nadia had 12 tests. She wanted 40. She was tired. She opened Claude and typed: "Write Vitest tests for this file. Cover all edge cases including error paths. Use AAA pattern."

Thirty seconds later, she had 25 tests. She read them carefully, deleted 8 that were trivial, fixed 3 that had wrong expectations, and committed 14. The whole process took 20 minutes.

Two years ago, that would have been 4 hours of work. Now it was 20 minutes plus review.

AI is genuinely excellent at generating test scaffolds. But the developers who get the most out of it are the ones who review the output carefully and know what to fix.

Prompts That Produce Useful Tests

🎯

Happy path + edge cases

"Write Vitest tests for this function. Cover: normal input, empty input, null, undefined, boundary values, and one happy-path async case."

🛡️

Error path coverage

"Write tests for every throw/reject path in this function. Include cases where a dependency rejects and where it throws synchronously."

🔬

Regression test from a bug

"Here is a bug report and the fix. Write a regression test that would have caught the original bug."

🎭

Mock scaffolding

"Generate Vitest mocks for this Express handler. Mock the DB, the logger, and the payment client. Show the full test file."

📊

Coverage gaps

"Here is my source and my tests. List the branches and edge cases I'm not testing."

🔄

Test refactoring

"Refactor these tests to reduce duplication. Extract shared setup into helper functions. Keep AAA pattern visible."

What AI Gets Wrong (and How to Catch It)

❌ Common AI test mistakes

  • Writing tests that always pass (no real assertions).
  • Testing implementation details instead of behavior.
  • Missing await on async assertions.
  • Using stale mocks across tests without resetting.
  • Asserting on internal state instead of observable behavior.
  • Mocking the thing you're supposed to be testing.
  • Not covering error paths — only the happy path.
  • Repeating the same test 10 times with tiny variations.

✅ Always check for

  • Every test has at least one meaningful assertion.
  • Tests fail when you intentionally break the source.
  • Mocks reset between tests (beforeEach).
  • Async tests are async and await properly.
  • Error paths tested with rejects.toThrow().
  • Test names describe behavior, not implementation.
  • Boundary values: 0, empty string, null, negative numbers.
  • Realistic data, not foo, bar, baz.
🤖

The mutation-testing trick: after AI writes tests, deliberately break the source code in 3 places. If the tests still pass, they're not really testing anything. This is called "mutation testing", and doing it manually takes 2 minutes while revealing which tests are actually valuable.

The Full AI Testing Workflow

ai-testing-workflow.txt Process
1. Write the function (or fix the bug)
2. Paste function into AI with this prompt:
   "Write Vitest tests for this function. Use AAA pattern.
    Cover: happy path, empty input, null, undefined, boundaries,
    each error path, and one async rejection case."

3. Review every test:
   - Is the assertion meaningful?
   - Would this fail if the code was broken?
   - Is it testing behavior or implementation?

4. Delete trivial / duplicate tests

5. Add the missing edge cases yourself
   (AI often misses domain-specific boundaries)

6. Run the tests
   - All pass on current code ✓
   - Deliberately break the code → tests fail ✓

7. Commit the ones you trust

Total time for a typical function: 10–20 minutes
(2–3× faster than writing tests by hand)

12 · Resolution — 04:19 AM, 3 Days Later

Three days after the incident. Nadia deployed the fixed version. This time, she wasn't anxious. The deploy button felt different — because the tests had already proven the fix, and 40 other behaviors besides.

She went to sleep at 11 PM. She slept through the night. The pager stayed quiet.

Three weeks later, the team hired two new engineers. They ran the test suite, read the TypeScript types, and understood the payment system in a day — no oral tradition, no Slack archaeology, no "ask Nadia".

The incident cost $18,000. The testing infrastructure cost $0 and a weekend. The math was not close.

What Changed in Nadia's Week

Before After
Zero tests 47 tests across unit / integration / E2E
Plain JavaScript TypeScript strict mode, 0 any types
Manual code review style debates ESLint + Prettier enforce everything
Deploys on Thursday evening = anxiety Deploys whenever, with confidence
Debugging = reading logs at 2 AM Debugging = a failing test with a clear error
Onboarding new engineers: 2 weeks Onboarding: 2 days
Refactoring felt dangerous Refactoring is now the safest kind of work

The Real Lesson

Nadia's week wasn't about becoming a "test expert" or a "TypeScript evangelist". It was about a shift in mindset that happens to every backend developer who crosses this threshold:

🧭

The professional shift:

Junior developers write code that works. Professional developers write code that keeps working — through refactors, migrations, new hires, changed requirements, and 3 AM rollouts. Tests, types, and linters are not overhead. They are the difference between a program and a product.

What To Do Tomorrow

You don't need to do everything Nadia did in three days. But you can do one thing today:

🎯

Pick one function

Any function. Something you wrote recently. Small, but real.

Write one test

Three lines: it('...', () => expect(fn(x)).toBe(y)). That's it.

🔧

Install one tool

Vitest OR Prettier OR checkJs: true. Just one, not all three.

🔄

Repeat tomorrow

Habits compound. Sixty small steps beat one big rewrite.

13 · Interactive Knowledge Check

Twelve questions covering every tool and pattern from Nadia's story. Each maps to a scenario you'll encounter in your first month of writing production-quality JavaScript.

🧠

Part 6 — Testing, TypeScript & Quality Quiz

Twelve questions. Every answer maps to a production scenario.
Score: 0 / 12

14 · Cheat Sheet & What's Next

Testing, Types & Quality — One-Page Summary

Concept One-Line Rule
Test pyramidMany unit tests, fewer integration, fewest E2E. Fast at the base, expensive at the top.
AAA patternArrange → Act → Assert. Reads naturally in every test framework.
Vitest vs JestVitest for new projects (faster, native ESM). Jest for existing suites.
Matchers you'll use 90% of the timetoBe, toEqual, toHaveBeenCalledWith, resolves, rejects.
StubReturns a fixed value.
SpyRecords how it was called.
MockHas expectations — must be called in a specific way.
FakeWorking simplified implementation (in-memory DB).
Always reset mocksafterEach(() => vi.restoreAllMocks()) — prevents leaks between tests.
CoverageMeasures execution, not correctness. Target 100% for money, 70% for the rest.
Branch coverageMore meaningful than line coverage — catches missed if/else paths.
Integration testsTest several modules together — real DB, real HTTP, mocked externals.
PlaywrightThe 2026 default for E2E. Auto-waits, great DX.
TypeScriptTypes as documentation. Zero runtime cost. Opt-in by file.
Discriminated unionsModel states with a tag field. Compiler enforces completeness.
Utility typesPartial, Pick, Omit, Record, Readonly.
Result type{ ok: true, value } | { ok: false, error } — forces error handling.
ZodRuntime validation + type inference from one schema.
JSDocTypes without a build step. Enable with checkJs: true.
PrettierFormatting — zero decisions. formatOnSave: true.
ESLintCode quality & bug-catching rules. Especially no-floating-promises.
BiomeAll-in-one formatter + linter. Rust-based, 20× faster.
AI for testsExcellent scaffold generator. Always review, then break source to verify.

Do / Don't — Quality Edition

✅ DO

  • Write tests when you fix a bug — non-negotiable.
  • Start with unit tests, not E2E.
  • Use beforeEach to reset state and mocks.
  • Enable checkJs or migrate to TypeScript incrementally.
  • Model state as discriminated unions, not optional fields.
  • Validate input at system boundaries with Zod (or similar).
  • Format on save — never debate style in code review.
  • Enable no-floating-promises in ESLint.
  • Use AI to scaffold tests, then review every assertion.
  • Deliberately break source to verify your tests catch it.

❌ DON'T

  • Don't aim for 100% coverage as a goal.
  • Don't test implementation details — test behavior.
  • Don't forget await on async assertions.
  • Don't share mock state between tests.
  • Don't use any in TypeScript — use unknown + narrow.
  • Don't add types everywhere at once — migrate file by file.
  • Don't skip error path tests.
  • Don't trust AI tests without verification.
  • Don't argue about tabs vs spaces — automate it.
  • Don't skip pre-commit hooks — they save CI time.

What's Coming in Part 7 — The Final Chapter

Part 7 closes the series with everything you need to ship real JavaScript to production:

  • AI-assisted development workflows — how senior engineers use Claude, Copilot, and Cursor.
  • Observability: structured logging, tracing, and metrics for JavaScript services.
  • Performance profiling in production — where JavaScript actually slows down.
  • Deployment strategies: blue-green, canary, feature flags.
  • Security best practices: input validation, secrets, CSP, CORS.
  • Building your personal "JS expert" toolkit — the reading list, tools, and habits.
  • A complete capstone project that ties together everything from Parts 1–6.
  • Where to go next: WebAssembly, edge runtime, Bun, Deno, and the future of JavaScript.
🎯

Practice before Part 7: pick the file in your codebase that has caused the most production headaches. Write three tests for it — one happy path, one edge case, one regression test for a bug you remember. Then add // @ts-check at the top of the file and fix the first error the compiler gives you.

You'll be shocked how much you learn in 30 minutes.


Part 6 of 7 · JavaScript for Backend Developers · FreeLearning365.com

🌟 Continue Learning on FreeLearning365

Free tools, tutorials, and question banks for developers, students, and professionals.

🌍 FreeLearning365.com — Your gateway to free learning, tools & resources.

Part 6 of 7 · JavaScript for Backend Developers · © 2026 FreeLearning365


🚀 JavaScript for Backend Developers
🗺️ The Complete 7-Part Journey
Navigate anywhere in the series
Learning Path Part 1 of 7
🏆 Part 7 → AI + Observability + Production + 3 Capstone Projects

No comments:

Post a Comment

Thanks for your valuable comment...........
Md. Mominul Islam