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:
{
"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.
// 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:
export function priceWithTax(price: number, taxRate: number = 0.08): number {
return price * (1 + taxRate);
}
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.
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
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):
- A CLI expense tracker — add, list and total expenses stored in a local JSON file, with categories and a monthly summary.
- 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:
- Set up the project with a real
package.json,tsconfig.json(strict: true), andbuild/test/lintnpm scripts. - Define at least two
interfaces ortypealiases for your core data shape (e.g. anExpenseorTodo), 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, orOmit) somewhere real. - 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.
- Use Node's
fsmodule (Week 10) to persist data to a local JSON file between runs, wrapped intry/catch(Week 8) for a missing or corrupt file. - 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).
- Run
npm run lint,npm testandnpm run buildall successfully with zero errors before considering it done.
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?
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?
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?
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?
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.