Week 14: Tooling, Testing & Capstone

The last week. Everything from Week 1's let and const through Week 13's generics gets used here — plus the tooling around real projects that thirteen weeks of single-file examples never needed: npm scripts, a linter, a bundler, and actual automated tests. It ends with the capstone: a small, tested, fully-typed project you design and build yourself.

Phase 8 of 8 Week 14 of 14 ~5–6 Hours Capstone Project

By the end of this week, you'll be able to

  • Wire up package.json scripts, ESLint, and a bundler for a real TypeScript project
  • Write and run unit tests for a pure function with Vitest
  • Design, build and test a small project using the full 14-week toolkit

1. npm Scripts & package.json

package.json's scripts section defines short, memorable names for the commands a project actually needs — instead of memorizing (or retyping) long CLI invocations:

package.json scripts
{
  "name": "capstone-project",
  "type": "module",
  "scripts": {
    "build": "tsc",
    "dev": "tsc --watch",
    "test": "vitest run",
    "lint": "eslint src"
  },
  "devDependencies": {
    "typescript": "^5.4.0",
    "vitest": "^1.4.0",
    "eslint": "^9.0.0"
  }
}

Running npm run build executes whatever command is under "build"tsc here, Week 11's compiler. Every teammate (or CI pipeline) runs the exact same npm test or npm run lint, regardless of what's actually installed globally on their machine, since npm resolves these to the versions listed in devDependencies. This is the standard shape of almost every real JavaScript/TypeScript project's entry point.

2. ESLint & a Bundler

Two more tools worth recognizing, even briefly: a linter catches likely mistakes and style inconsistencies automatically, and a bundler packages many source files into the few files a browser actually needs to load efficiently.

a minimal ESLint config
// eslint.config.js
export default [
  {
    rules: {
      "no-unused-vars": "warn",   // flag a declared-but-never-used variable
      "eqeqeq": "error",          // require === instead of == (Week 1's coercion, enforced)
      "no-var": "error",           // require let/const over var (Week 2/3)
    },
  },
];

ESLint reads a project's source files and reports problems without running any of the code — catching things like an unused variable, an accidental == instead of ===, or a lingering var, automatically and consistently across an entire codebase. A bundler (Vite, esbuild, webpack) solves a different problem: a browser loading dozens of separate ES module files one by one is slow, so a bundler combines and optimizes them into a small number of output files before deployment. Setting one up fully is beyond this week's scope — the important thing is knowing what problem each tool solves and recognizing their configs on sight.

3. Your First Tests With Vitest

A test is code that checks other code — Vitest is a modern test runner that pairs naturally with TypeScript and the pure-function habits from Week 2:

math.ts -- the function being tested
export function priceWithTax(price: number, taxRate: number = 0.08): number {
  return price * (1 + taxRate);
}
math.test.ts -- the test
import { describe, it, expect } from "vitest";
import { priceWithTax } from "./math";

describe("priceWithTax", () => {
  it("applies the default 8% tax rate", () => {
    expect(priceWithTax(100)).toBeCloseTo(108);
  });

  it("applies a custom tax rate", () => {
    expect(priceWithTax(100, 0.2)).toBeCloseTo(120);
  });

  it("returns the original price when tax rate is 0", () => {
    expect(priceWithTax(50, 0)).toBe(50);
  });
});

describe groups related tests under a label; each it is one individual test case, naming the specific behavior being checked. expect(actual).toBe(expected) is the core assertion — toBeCloseTo is the floating-point-safe version, useful whenever rounding could make an exact === comparison unreliable. Running npm test executes every it block and reports which passed and which failed, without ever needing to manually run the function and eyeball the output.

Pure functions are why Week 2 kept insisting on them

priceWithTax is trivial to test precisely because it's pure: same inputs, same output, no external state to set up or tear down. A function with side effects needs its environment carefully staged before every test and inspected after — pure functions just need an input and an expected output.

4. Capstone Project

Capstone

Build a small, typed, tested command-line tool

A project that draws on every phase of this course — pick one of the two briefs below, or design your own of similar scope.

Suggested briefs (pick one):

  1. A CLI expense tracker — add, list and total expenses stored in a local JSON file, with categories and a monthly summary.
  2. A CLI todo manager — add, complete, filter by status, and persist todos to a local JSON file between runs.

Requirements, regardless of which brief you choose:

  1. Set up the project with a real package.json, tsconfig.json (strict: true), and build/test/lint npm scripts.
  2. Define at least two interfaces or type aliases for your core data shape (e.g. an Expense or Todo), using what you need from Weeks 11–13: optional/readonly properties, a union or enum for status/category, and at least one utility type (Partial, Pick, or Omit) somewhere real.
  3. Write your core logic as small, pure functions wherever possible (Week 2, Week 6) — a function that adds an item should return a new array, not mutate one in place.
  4. Use Node's fs module (Week 10) to persist data to a local JSON file between runs, wrapped in try/catch (Week 8) for a missing or corrupt file.
  5. Write at least 5 Vitest unit tests covering your pure functions, including at least one edge case (an empty list, a not-found ID, or similar).
  6. Run npm run lint, npm test and npm run build all successfully with zero errors before considering it done.
Where to go from here

This capstone is intentionally small enough to finish, and intentionally close to a real, shippable tool. It's also a genuine portfolio piece — a typed, tested CLI with a clean git history (see this site's Git & GitHub course) is exactly the kind of small, complete project that's worth linking from a resume.

5. Knowledge Check

Four quick questions. Expand each to check your answer.

Q1

Why does defining "test": "vitest run" in package.json's scripts matter, instead of everyone just remembering to run vitest directly?

An npm script gives every contributor (and any CI pipeline) one consistent, memorable command — npm test — that resolves to the exact locally-installed version of the tool listed in devDependencies, rather than relying on whatever's globally installed on each person's machine, which could be a different version or missing entirely.

Q2

What's the difference between what a linter (ESLint) checks and what the TypeScript compiler checks?

The TypeScript compiler checks that values match their declared types — it would catch passing a string where a number is expected. A linter like ESLint checks code-quality and style concerns that aren't about types at all — an unused variable, a stray == instead of ===, or a lingering var. Both are useful and address different classes of mistakes.

Q3

Why is priceWithTax from this lesson's testing example easy to write tests for, compared to a function that reads from a database?

priceWithTax is a pure function (Week 2): given the same inputs, it always produces the same output, and it touches nothing outside itself. A test just calls it and checks the result — no setup or teardown of any external state required. A function that reads from a database has a side effect and a dependency on external state, so testing it properly requires additional work to set up (or fake) that database first.

Q4

Why does the capstone's requirements list specifically call for storing your core logic as pure functions?

Pure functions are what make the "write 5 Vitest unit tests" requirement realistic in the first place — a pure "add expense" function that returns a new array can be tested directly with plain inputs and expected outputs, exactly like priceWithTax. If that same logic mutated an array in place or touched the filesystem directly, each test would need considerably more setup to isolate and verify.