# Capstone: Build a Report Card Service

Course: [Node.js](https://www.devtutorials.ai/learn/nodejs) · Section: Project and Reference
Source: https://www.devtutorials.ai/learn/nodejs/project-reference/capstone

> A complete Node.js project in five steps: a command-line tool with parseArgs and an env file, streaming a marks file with readline and saving results safely, an EventEmitter-based report builder with custom errors, an HTTP API with health checks and graceful shutdown, and tests with node:test, followed by three extension exercises.

## Capstone: Build a Report Card Service

**Capstone** = one project that uses what the whole course taught.

You will build **Report Card Service** for Greenfield School:

- A command-line tool that reads a marks file and builds report cards.
- A small web API that serves the reports.
- Tests that check it all.

**Simple lesson:** a real project is many small lessons joined together.

### A Real-World Example

At the end of every term, the class teacher at Greenfield School:

- gets a marks sheet with one row per student and subject,
- adds up each student's marks and works out the grade,
- finds the topper and the students who need help,
- shares the results with parents,
- and checks that nothing was copied wrongly.

Your program will do the same job in less than a second.

## What You Will Learn

In this lesson, you will:

1. Plan the project as small modules
2. Build the command line with `parseArgs` and a `.env` file
3. Stream the marks file with `readline` and save results safely
4. Report progress with events and catch bad rows with a custom error
5. Serve reports over HTTP with health checks and graceful shutdown
6. Test the grades, the builder and the server with `node:test`
7. Extend the project in three exercises

## The Plan

<figure className="ngc-fig node-fig">
<svg viewBox="0 0 560 230" role="img" ariaLabelledby="fig-node-capstone" style={{ width: "100%", maxWidth: 560, height: "auto" }}>
<title id="fig-node-capstone">Diagram: marks.csv is streamed by the ReportBuilder, which uses grades.ts, emits events and writes report.json, which the CLI prints and the HTTP server serves, with tests checking both</title>
<rect x="15" y="90" width="100" height="44" rx="8" opacity="0.12"  style={{ fill: "var(--ngc-slate)", stroke: "var(--ngc-slate)" }}/><text x="65" y="117" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12 }}>marks.csv</text>
<rect x="150" y="80" width="140" height="64" rx="10"  style={{ fill: "var(--ngc-purple-soft)", stroke: "var(--ngc-purple)", strokeWidth: 1.5 }}/><text x="220" y="106" textAnchor="middle" style={{ fill: "var(--ngc-purple)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>ReportBuilder</text><text x="220" y="124" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--sans)", fontSize: 11 }}>streams, events, errors</text>
<rect x="150" y="10" width="140" height="44" rx="8"  style={{ fill: "var(--ngc-orange-soft)", stroke: "var(--ngc-orange)", strokeWidth: 1.5 }}/><text x="220" y="37" textAnchor="middle" style={{ fill: "var(--ngc-orange)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>grades.ts</text>
<rect x="325" y="90" width="100" height="44" rx="8" opacity="0.12"  style={{ fill: "var(--ngc-slate)", stroke: "var(--ngc-slate)" }}/><text x="375" y="117" textAnchor="middle" style={{ fill: "var(--ink)", fontFamily: "var(--mono)", fontSize: 12 }}>report.json</text>
<rect x="455" y="40" width="95" height="44" rx="8"  style={{ fill: "var(--ngc-blue-soft)", stroke: "var(--ngc-blue)", strokeWidth: 1.5 }}/><text x="502" y="67" textAnchor="middle" style={{ fill: "var(--ngc-blue)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>CLI</text>
<rect x="455" y="140" width="95" height="44" rx="8"  style={{ fill: "var(--ngc-green-soft)", stroke: "var(--ngc-green)", strokeWidth: 1.5 }}/><text x="502" y="167" textAnchor="middle" style={{ fill: "var(--ngc-green)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>HTTP API</text>
<rect x="150" y="180" width="140" height="40" rx="8"  style={{ fill: "var(--ngc-red-soft)", stroke: "var(--ngc-red)", strokeWidth: 1.5 }}/><text x="220" y="205" textAnchor="middle" style={{ fill: "var(--ngc-red)", fontFamily: "var(--sans)", fontSize: 13, fontWeight: 700 }}>tests (node:test)</text>
<line x1="115" y1="112" x2="150" y2="112"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/><line x1="220" y1="54" x2="220" y2="80"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/><line x1="290" y1="112" x2="325" y2="112"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/>
<line x1="425" y1="105" x2="455" y2="62"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/><line x1="425" y1="120" x2="455" y2="162"  style={{ stroke: "var(--ngc-slate)", strokeWidth: 2 }}/><line x1="220" y1="144" x2="220" y2="180"  style={{ stroke: "var(--ngc-red)", strokeWidth: 1.5, strokeDasharray: "4 4" }}/>
<circle r="6" cx="115" cy="112" style={{ fill: "var(--ngc-purple)" }}><animate attributeName="cx" values="115;150;290;325;425;455" keyTimes="0;0.15;0.45;0.6;0.8;1" dur="5s" repeatCount="indefinite" /><animate attributeName="cy" values="112;112;112;112;112;162" keyTimes="0;0.15;0.45;0.6;0.8;1" dur="5s" repeatCount="indefinite" /></circle>
</svg>
<figcaption>Data flows from the marks file through the builder into a saved report, which the CLI and the API share.</figcaption>
</figure>

The files:

- **`src/grades.ts`** = small pure functions: total, average, grade. Easy to test.
- **`src/report.ts`** = the `ReportBuilder`: reads the CSV as a stream, emits events, throws `BadRowError`.
- **`src/config.ts`** = reads and checks settings.
- **`cli.ts`** = the command-line tool.
- **`server.ts`** = the HTTP API.
- **`test/*.test.ts`** = the tests.

Build one step at a time, and run it after every change.

**Simple lesson:** keep pure logic, input and output, and the entry points in separate modules.

## Step 1: Command Line and Config

- `parseArgs` reads `--class`, `--term` and `--help` (see **Process, OS and CLI Tools**).
- `--env-file=.env` gives `PASS_MARK` and `SCHOOL_NAME`.
- `loadConfig` checks the settings and fails fast.

**Simple lesson:** read options and settings once, check them, and stop early if anything is wrong.

## Step 2: Stream the Marks and Save Safely

- The marks file can be big, so read it line by line with `readline` (see **Buffers, Streams and Compression**).
- `grades.ts` holds tiny pure functions.
- The result is saved with write-temp-then-rename (see **Path and File System**).

**Simple lesson:** stream the input, keep the maths in pure functions, and save with a rename.

## Step 3: Events and Custom Errors

- The `ReportBuilder` extends `EventEmitter` (see **Events and Utilities**).
- It emits `student` for each finished card, `bad-row` for broken lines, and `done` at the end.
- A broken line throws a `BadRowError` with a code and the line number (see **Error Handling**).
- The CLI listens to the events, so the builder never prints anything itself.

The same `ReportBuilder` could feed the HTTP API, a test or an email job, because it does not print.

**Simple lesson:** the builder announces what happens, and the caller decides what to print.

## Step 4: The HTTP API

- `GET /health` = liveness for the platform (see **Production Readiness**).
- `GET /reports/:class` = the saved report as JSON, or 404.
- `SIGTERM` = close the server gracefully.

**Simple lesson:** a small API needs routes, JSON errors, a health check and a clean shutdown.

## Step 5: Tests

- Pure functions are tested directly.
- The builder is tested with a temp file and its events.
- Run them all with `node --test` (see **Testing with the Built-in Test Runner**).

**Simple lesson:** test the pure parts directly, and test the stream parts with temp files.

## Running It on Your Own Computer

1. Make a folder `report-card` and copy the files from the steps into it.
2. Run `node --env-file=.env cli.ts --class 8` to build the report.
3. Run `node --test` to check everything.
4. Run `node server.ts` and open `http://localhost:3000/reports/8`.
5. Add `"bin"` to `package.json` and run `npm link`, so `report-card` works anywhere.

Running `.ts` files directly needs a recent Node.js version. On older versions, run the JavaScript files, or use `npx tsx`.

## What You Used From Each Chapter

- **Foundations:** ES modules, `.ts` files, `package.json` with `type` and `bin`.
- **Asynchronous Node.js:** `await`, events, custom errors.
- **System and Files:** `parseArgs`, env files, `fs/promises`, `readline` streams, safe writes.
- **Networking:** an HTTP API with JSON and 404s.
- **Quality and Production:** tests, health checks, graceful shutdown.

## Common mistakes

<div className="tscroll">

| Mistake | Wrong idea | Correct idea | What goes wrong |
|---|---|---|---|
| One big file for everything | Small projects need no structure | Split pure logic, I/O and entry points | Hard to test and change |
| Printing inside the builder | Every part can log | Emit events, let the caller print | The builder cannot be reused in the API |
| Stopping at the first bad row | Bad data must stop everything | Skip it, report it and keep going | One typo blocks the whole class report |
| Reading the whole CSV with `readFile` | Files are always small | Stream it with `readline` | Big files fill memory |
| No test for bad input | Happy paths are enough | Test broken rows too | Bugs hide in the edge cases |

</div>

## TL;DR

1. **Plan** = pure functions, a builder, config, CLI, server, tests.
2. **CLI** = `parseArgs` + `--env-file` + fail-fast config.
3. **Data** = `readline` stream in, write-temp-then-rename out.
4. **Builder** = `EventEmitter` + `BadRowError` with a code and line.
5. **API** = routes, JSON errors, `/health`, SIGTERM shutdown.
6. **Tests** = `node:test` for functions, temp files for streams.

## Check Yourself

## Exercises

## What to Remember

- Split a project into pure logic, I/O, entry points and tests.
- Check options and settings at start-up.
- Stream input, report bad data with custom errors, and keep going.
- Emit events from reusable parts, and let callers decide what to show.
- Give every API a health check and a graceful shutdown.
- Test the pure parts directly and the I/O parts with temp files.

Next: **Node.js Cheat Sheet**. Keep it open while you build your own projects.
