Run TypeScript on the server with Node and Express
Outcome
Section titled “Outcome”You will add the first server process to Delivery Board. Node will run TypeScript outside the browser. Express will match GET /api/health, call a route handler, and send a JSON response with HTTP status 200.
The React client and Express server will remain separate processes. The client will still use its in-memory fixture. This lesson establishes the runtime, HTTP, configuration, and file boundaries that the next lesson will use for work-item routes, runtime validation, and automated API tests.
Why this matters
Section titled “Why this matters”A browser application cannot safely own every responsibility. Secrets, shared records, authorization decisions, and persistent changes need a trusted environment that the application owner controls. Server-side code receives requests from clients, checks untrusted input, performs allowed work, and returns a response.
React, Express, and PHP can all participate in a web application, but they run in different places and at different times. Understanding those boundaries matters more than memorizing one framework method. The request, response, route, validation, and status-code concepts transfer to PHP and other server stacks.
What you will practice
- Distinguish source language, runtime, process, server, framework, and API responsibilities.
- Trace an HTTP request through method, path, route matching, handler execution, status, headers, and response body.
- Explain which browser APIs and Node APIs exist in each runtime.
- Run TypeScript directly with Node 24 while keeping type checking as a separate command.
- Configure one Express application module and one process entry module.
- Read a port from process.env and validate it before the server listens.
- Inspect a successful JSON response and an unmatched route without involving React.
- Compare the long-running Node process with a common request-scoped PHP execution model.
- Document the server start, stop, verification, and recovery path for another contributor.
What is new and what is reused
Section titled “What is new and what is reused”- New: Server runtime, process, HTTP server, origin, port, request, response, route, route handler, status code, response header, JSON response, environment variable,
process.env, Express application,app.get,response.status,response.json, listener, Node type stripping,NodeNext, and separate application and process modules. - Reused: The accepted Delivery Board
mainbranch, TypeScript types and modules, npm dependencies and scripts, strict type checking, Git branches, browser developer tools, JSON syntax, accessible React client, automated client tests, production build, and focused commits.
Starting point
Before you start
- Project stage 2 is accepted on remote main and the normal working copy is clean.
- Node 24 LTS, npm, Git, and the project dependencies are available.
- The repository package.json already declares type module and provides client dev, test, typecheck, lint, build, and preview commands.
- The React client passes its complete test and production-build gate before server packages are installed.
- Use fictional data only. No credential, secret, private person, real work record, database, authentication rule, deployment target, or public server is required.
- One terminal can remain occupied by a running process while a second terminal sends requests and runs checks.
- Current state
- Remote main contains the accepted React client. No project-owned server process, server TypeScript configuration, API route, or server verification command exists.
- First action
- Synchronize main, run the accepted Stage 2 gate, create feature/express-api, and write one request-response trace for GET /api/health in README.md before installing Express.
- First checkpoint
- A Node process runs server/index.ts, GET /api/health returns the documented 200 JSON response, an unknown route remains visibly unmatched, and the React client still passes its existing checks.
- Help trigger
- Use the nearest recovery note or ask for help if the current branch or accepted baseline is unclear, Node is not version 24, npm cannot find package.json, a TypeScript file uses syntax that Node cannot remove, an import cannot be resolved, the selected port is occupied, a request never receives a response, the reported response differs from the route contract, or stopping one process also stops an unrelated process.
Required result
Section titled “Required result”You have completed the lesson when:
feature/express-apibegins from the synchronized and verified Stage 2maincommit;expressis a production dependency and@types/expressplus@types/nodeare development dependencies;package-lock.jsonrecords the resolved dependency graph;tsconfig.server.jsonchecksserver/**/*.tswith strict Node ESM rules and no emitted output;- the existing
typecheckandbuildcommands include the server TypeScript check without removing the client checks; dev:serverstarts the server with Node watch mode andstart:serverstarts one normal server process;server/app.tsexports one Express application without opening a network port;- the application disables the unnecessary
X-Powered-Byresponse header; GET /api/healthreturns status200and the exact JSON body{ "status": "ok", "service": "delivery-board-api" };- the health response has a JSON content type and
Cache-Control: no-store; server/index.tsreadsPORT, uses3000when it is absent, and rejects a non-integer or out-of-range value before listening;- the server listens on
127.0.0.1for this local lesson and logs its complete local URL; - a direct request proves the health route without using the React client;
- an unmatched route returns a non-success result and is not mistaken for a working API route;
- one controlled invalid-port check fails for the expected configuration reason, and the environment variable is removed afterward;
- client tests, complete type checking, linting, and the production client build still pass;
- README names the two runtimes, server commands, health contract, stop action, port recovery, and the fact that the client is not connected yet; and
- the final focused commit contains only the server foundation, package records, configuration, scripts, and matching documentation.
Establish the accepted branch baseline
Section titled “Establish the accepted branch baseline”Open PowerShell in the Delivery Board folder. Synchronize the accepted branch before creating server work:
git switch maingit pull --ff-onlygit statusnpm cinpm testnpm run typechecknpm run lintnpm run buildgit switch -c feature/express-apigit statusThe baseline must be clean. If a Stage 2 check fails, stop and identify whether the working copy, installed dependency graph, or accepted source differs from the reviewed result. Do not install a server package to repair an unrelated client failure.
If feature/express-api already exists, inspect it before continuing:
git branch --listgit log --oneline --decorate --graph --all -12git statusContinue the existing branch only when its base and purpose match this server stage. Do not create a near-duplicate branch name to avoid understanding the history.
Record the first request-response trace
Section titled “Record the first request-response trace”Add this contract to the README before code:
Request- Method: GET- Path: /api/health- Body: none
Response- Status: 200 OK- Content-Type: application/json- Cache-Control: no-store- Body: {"status":"ok","service":"delivery-board-api"}
Current boundary- The Express route proves that the local server is available.- The React client does not request server data yet.- Work-item API routes, request-body validation, API tests, persistence, authentication, and deployment are not part of this lesson.This is an observable contract. It tells another contributor what request to send and what evidence counts as success.
Build a precise runtime model
Section titled “Build a precise runtime model”TypeScript is the source language for both sides of this project. That does not make the runtimes the same.
| Question | React client | Express server |
|---|---|---|
| Where does project code run? | In the visitor’s browser | In a Node process controlled by the application owner |
| What starts it? | Loaded client assets and browser navigation | A server command such as node server/index.ts |
| Which global APIs are expected? | window, document, browser history, DOM events |
process, environment variables, file and network APIs |
| What can the user inspect or change? | Downloaded client source and every client request | Requests sent to the server, but not private server source or environment values |
| How long can state live? | Until the tab, navigation, or application replaces it | Potentially across many requests while the process runs |
| Can it trust incoming data? | No | No; the server must validate every external boundary |
| Current Delivery Board responsibility | Render and update the local interface | Report that the API process is available |
Do not put a secret in React source, a VITE_ environment variable, built client JavaScript, or any value returned to the browser. The user controls the browser environment. A future secret belongs only in a protected server environment, and server code must avoid sending it in a response or log.
Source language is not runtime evidence
Section titled “Source language is not runtime evidence”Node 24 can remove supported TypeScript type syntax and run the remaining JavaScript directly. This operation is type stripping. Node does not read the project tsconfig.json to prove that assignments satisfy the declared types.
These commands answer different questions:
| Command | Question answered |
|---|---|
node server/index.ts |
Can Node remove the supported type syntax and run this server process? |
tsc --project tsconfig.server.json |
Does the server source satisfy the configured TypeScript rules? |
npm run lint |
Does the source satisfy the repository’s configured static rules? |
| A direct HTTP request | Does the running process return the required runtime response? |
A process can start even when the type checker would report a mistake. Always run both runtime and static checks.
How this relates to PHP
Section titled “How this relates to PHP”PHP is a valid server-side choice. A common PHP setup starts or resumes PHP execution for one incoming web request, makes request data available to the script or framework, produces a response, and ends that request’s execution. A common Node and Express setup starts one long-running process, registers route handlers once, and calls a matching handler for each request.
| Transferable concept | Express form in this lesson | Common PHP form |
|---|---|---|
| Route | app.get('/api/health', handler) |
Web-server or framework route mapped to a PHP handler |
| Request data | Express request object |
Framework request object or PHP request data such as $_GET |
| Status | response.status(200) |
Framework response API or http_response_code(200) |
| JSON body | response.json(value) |
Framework JSON response or json_encode(value) plus a content-type header |
| Configuration | process.env.PORT |
Environment access such as getenv('PORT') |
| Reusable code | ESM imports and exports | Includes, autoloaded classes, functions, or framework services |
The syntax and process lifecycle differ. The engineering questions remain recognizable: Which route matches? What input arrived? Is it valid? Which result is allowed? What status and body should the server send? What evidence proves the behavior?
Do not describe one stack as universally better. This course uses TypeScript to keep one source-language model across the React client, Express server, and later Capacitor shell. The server concepts still transfer.
Checkpoint: The runtime and HTTP boundary is documented
- What now works
- The branch begins from accepted Stage 2, README defines one exact health request and response, and the runtime comparison distinguishes browser code, the Node process, type checking, and a common PHP request model.
- Files changed
README.md, Git branch history- What remains
- Install Express, add the server TypeScript configuration, and implement the documented route.
- Next action
- Install Express and its required development-time type packages from the repository root.
- If it does not work
- Check remote main, the current branch, the accepted client gate, and the written health contract before changing package files.
Install the smallest server dependency set
Section titled “Install the smallest server dependency set”Run these commands from the folder that contains the existing package.json:
npm install expressnpm install --save-dev @types/express @types/nodeUse the installed current Express 5 release. Express is a runtime dependency because the server imports it while the process runs. The two @types packages are development dependencies because they describe JavaScript APIs to TypeScript and do not provide the running Express code.
Express is written in JavaScript and does not bundle its own TypeScript declarations. @types/express supplies community-maintained declarations. @types/node describes Node globals and built-in modules such as process.
Inspect the result:
npm ls express @types/express @types/node typescriptgit diff -- package.json package-lock.jsonDo not copy a version number from this page into the lockfile. npm must resolve and record the supported package graph at installation time. Commit both package files later.
Add a server TypeScript configuration
Section titled “Add a server TypeScript configuration”Create tsconfig.server.json in the repository root:
{ "compilerOptions": { "target": "ESNext", "module": "NodeNext", "types": ["node"], "rewriteRelativeImportExtensions": true, "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "noEmit": true, "strict": true, "skipLibCheck": true, "forceConsistentCasingInFileNames": true }, "include": ["server/**/*.ts"]}The client and server need different module-resolution models:
- Vite bundles client modules, so the client configuration uses bundler-aware rules.
- Node loads server modules at runtime, so the server uses
NodeNext. noEmitkeeps this configuration focused on static checking because Node runs the supported TypeScript source directly.erasableSyntaxOnlyrejects TypeScript syntax that Node cannot execute by removing types.verbatimModuleSyntaxmakes type-only and runtime imports explicit.rewriteRelativeImportExtensionssupports.tssource paths when a later build emits JavaScript. This lesson does not emit server files.
The root package.json already declares "type": "module". Node therefore treats project .ts files as ECMAScript modules. Use import and export, not require and module.exports.
Add server scripts without removing client checks
Section titled “Add server scripts without removing client checks”Update only the matching entries in package.json:
{ "scripts": { "dev": "vite", "dev:server": "node --watch server/index.ts", "start:server": "node server/index.ts", "typecheck": "tsc -b && tsc --project tsconfig.server.json", "build": "npm run typecheck && vite build" }}Keep the existing test, test:watch, lint, and preview scripts unchanged. Your package.json contains other properties and dependencies; do not replace the complete file with this partial example.
dev:server watches imported server files and restarts the process after a saved change. It does not type-check. start:server runs one normal process without watch behavior. The complete typecheck command checks the existing TypeScript project references first and the new server source second.
Run the static gate after the server files exist. Until then, tsconfig.server.json can report that no inputs were found.
Separate the Express application from the network listener
Section titled “Separate the Express application from the network listener”Create this structure:
Directoryserver/
- app.ts — Configure and export the Express application
- index.ts — Validate process configuration and open the network listener
Directorysrc/ — Existing React client source
- …
- package.json
- package-lock.json
- tsconfig.server.json
This separation creates a useful test seam. A later API test can import app and send an in-process request without opening a real port. The process entry remains responsible for configuration and listening.
Create the application module
Section titled “Create the application module”Create server/app.ts:
import express, { type Express, type Request, type Response,} from 'express';
export const app: Express = express();
app.disable('x-powered-by');
app.get('/api/health', (_request: Request, response: Response) => { response .status(200) .set('Cache-Control', 'no-store') .json({ status: 'ok', service: 'delivery-board-api', });});The module performs three jobs:
- It creates one Express application.
- It removes an unnecessary implementation-identifying response header.
- It registers one handler for the
GETmethod and/api/healthpath.
The underscore in _request communicates that Express supplies the request object but this handler does not read it. The response chain sets the status, adds an explicit cache policy for live health information, and serializes the object as JSON.
Express sets an appropriate JSON content type. JSON has no TypeScript types at runtime. The response object here is a trusted value created inside the server module.
Do not add express.json() yet. This lesson has no route that accepts a request body. The next lesson will add body parsing directly before routes that need it and will validate the parsed unknown value.
Trace the route handler
Section titled “Trace the route handler”For GET http://127.0.0.1:3000/api/health, the expected control flow is:
- The operating system delivers the TCP connection to the Node process listening on port
3000. - Node parses the HTTP message into request and response objects.
- Express checks registered middleware and routes in order.
- The method
GETand path/api/healthmatch the handler. - The handler creates the selected response status, headers, and JSON body.
- Express and Node send the response bytes to the client.
- The process remains available for another request.
The handler does not render React, read a database, or create a new Node process. One running process can handle many request-response cycles.
Validate process configuration and listen
Section titled “Validate process configuration and listen”Create server/index.ts:
import { app } from './app.ts';
const DEFAULT_PORT = 3000;const rawPort = process.env.PORT ?? String(DEFAULT_PORT);const port = Number(rawPort);
if (!Number.isInteger(port) || port < 1 || port > 65_535) { throw new Error( `PORT must be a whole number from 1 to 65535. Received: ${rawPort}`, );}
app.listen(port, '127.0.0.1', () => { console.log(`Delivery Board API: http://127.0.0.1:${port}`);});process.env.PORT has the type string | undefined. Environment variables enter a process as text even when the text represents a number. The code therefore:
- selects the environment value or the string form of
3000; - converts the text to a number;
- checks that the number is an integer inside the valid TCP port range; and
- listens only after the check passes.
This is runtime validation. A TypeScript annotation such as const port: number cannot prove that external text contains a usable number.
The host 127.0.0.1 accepts connections from the same computer. Do not change it to 0.0.0.0, expose the port to a network, or configure router forwarding for this lesson. Local-only listening reduces the current exposure while the API has no production controls.
Check the server types
Section titled “Check the server types”Run:
npm run typechecknpm run lintExpected result:
- the existing client project references pass;
tsconfig.server.jsonfinds both server files;- Express and Node imports have types;
- the
.tsrelative import is accepted; and - no client or server lint error remains.
If the import reports that ./app.ts is not allowed, confirm that the server configuration contains rewriteRelativeImportExtensions and that this file belongs to tsconfig.server.json. If Express has no declarations, confirm @types/express is installed as a development dependency.
Checkpoint: The server boundary type-checks without opening a port
- What now works
- The application module owns Express and the health route, the process entry owns environment validation and listening, and the complete client-plus-server static gate passes.
- Files changed
server/app.ts, server/index.ts, tsconfig.server.json, package.json, package-lock.json- What remains
- Run the process, inspect real HTTP evidence, test one configuration failure, and document recovery.
- Next action
- Start npm run dev:server in Terminal 1 and keep that terminal visible.
- If it does not work
- Check package installation, package type module, the exact .ts import, server include path, and first TypeScript diagnostic in that order.
Run the server and inspect HTTP evidence
Section titled “Run the server and inspect HTTP evidence”Open Terminal 1 in the repository root:
npm run dev:serverExpected output includes:
Delivery Board API: http://127.0.0.1:3000The terminal remains occupied because the server process is waiting for requests. Do not start another server because the prompt did not return.
Open Terminal 2 in the same project folder. Request the health route with the executable name curl.exe so PowerShell does not substitute a different command:
curl.exe -i http://127.0.0.1:3000/api/healthThe response must include evidence equivalent to:
HTTP/1.1 200 OKContent-Type: application/json; charset=utf-8Cache-Control: no-store
{"status":"ok","service":"delivery-board-api"}Header order and other transport headers can differ. The status, JSON content type, explicit cache policy, and body values are the contract.
Open the same URL in a browser. Confirm that the browser displays the JSON response and that its Network panel reports status 200. The address bar request goes directly to Express; React is not involved.
Confirm that route matching is specific
Section titled “Confirm that route matching is specific”Request a path that is not registered:
curl.exe -i http://127.0.0.1:3000/api/missingExpress currently returns its default 404 response. That is useful negative evidence: a running server does not make every path successful. The next lesson will replace this default with a consistent JSON error contract.
Confirm that neither response includes X-Powered-By. Do not treat header removal as complete security; it is one small information-reduction measure. Secure software also needs validated input, safe dependencies, authorization where required, careful error output, protected configuration, transport controls, and deployment review.
Keep the two development processes distinct
Section titled “Keep the two development processes distinct”If you also inspect the React client, use separate terminals:
| Terminal | Command | Runtime responsibility |
|---|---|---|
| 1 | npm run dev:server |
Express server at http://127.0.0.1:3000 |
| 2 | npm run dev |
Vite client development server at its reported URL |
| 3 or reused prompt | curl.exe, typecheck, tests, lint, and build |
One-time verification commands |
The two URLs are different origins because their ports differ. An origin consists of scheme, host, and port. The React client still does not send a request to the API. The API-consumption lesson will use a Vite development proxy and a relative /api URL rather than adding an unrestricted cross-origin policy.
Stop only the process you started
Section titled “Stop only the process you started”Focus Terminal 1 and press Ctrl+C. Then repeat the request:
curl.exe -i http://127.0.0.1:3000/api/healthThe connection must fail because no lesson server is listening. A failed connection is different from an HTTP 404: no HTTP response exists when the process is stopped.
Do not kill all Node processes. Another project or contributor can own a different process. Stop the exact terminal process you started.
Prove that external configuration needs runtime validation
Section titled “Prove that external configuration needs runtime validation”With the normal server stopped, set an invalid port in PowerShell:
$env:PORT = '70000'npm run start:serverThe process must stop with this focused error:
PORT must be a whole number from 1 to 65535. Received: 70000No server must start. Remove the temporary environment value even if the expected check passed:
Remove-Item Env:PORTnpm run start:serverThe normal 3000 URL must return. Stop the process with Ctrl+C after the check.
This controlled failure proves three things:
- environment input is a runtime boundary;
- TypeScript cannot validate the text before the process receives it; and
- the server rejects invalid configuration before it claims to be available.
If port 3000 is already occupied, do not stop an unidentified process. Inspect the owning terminal first. If another known project must keep that port, use a valid temporary value:
$env:PORT = '3100'npm run start:serverRequest the exact logged URL, stop the server, and run Remove-Item Env:PORT. Record the changed verification port in README. Do not hard-code the temporary value in source.
Checkpoint: The health contract works through a real HTTP request
- What now works
- The running process returns the documented 200 JSON contract, an unmatched path returns 404, stopping the process causes a connection failure, and invalid PORT text prevents listening.
- Files changed
Running Node process, HTTP response evidence, README.md- What remains
- Run the complete repository gate, update the handoff documentation, and create the focused foundation commit.
- Next action
- Update README with the verified commands, exact result, current limits, and server recovery path.
- If it does not work
- Separate process state, connection result, HTTP status, route match, response headers, and response body before editing source.
Document and commit the foundation
Section titled “Document and commit the foundation”Update README with:
- the browser and Node runtime responsibilities;
- Express’s application-module and process-entry responsibilities;
- required Node and npm versions;
- dependency installation through
npm ciafter cloning; dev:serverandstart:servercommands;- how to stop the exact server with Ctrl+C;
- the default local URL and valid
PORToverride; - the complete health request and response contract;
- the unmatched-route result;
- the client and server static-check command;
- the latest verified Node, Express, operating-system, route, status, header, and body evidence;
- recovery for an occupied port and an unresolved
.tsimport; - the difference between an HTTP error response and a failed connection; and
- current limits: no client request, work-item API, body parser, API tests, persistence, authentication, CORS policy, public network exposure, or deployment.
Run the complete gate with no server process active:
npm testnpm run typechecknpm run lintnpm run buildgit status --shortgit diff --checkgit diffStart npm run start:server once more, repeat the health and missing-route requests, stop the exact process, and confirm that port 3000 no longer accepts a connection.
Stage only the foundation files and matching documentation:
git add package.json package-lock.json tsconfig.server.jsongit add server/app.ts server/index.ts README.mdgit diff --cached --checkgit diff --cachedgit commit -m "feat: add Express server foundation"git statusThe final status must be clean on feature/express-api. Do not push or open the Stage 3 pull request yet. The branch will continue through API development, API consumption, and the Stage 3 assignment.
Self-check
Complete these checks against the required result.
- Confirm that feature/express-api begins from the synchronized and accepted Stage 2 main commit.
- Point to the README request-response contract that existed before the route implementation.
- Explain the difference between TypeScript as source, Node as runtime, Express as framework, and the API as an HTTP contract.
- Name one browser API that server code does not use and one Node API that browser client code cannot rely on.
- Explain why a downloaded React client cannot protect a secret.
- Compare the long-running Node process with a common request-scoped PHP execution model without claiming that either model is universal.
- Confirm that express is a production dependency while @types/express and @types/node are development dependencies.
- Explain why package.json and package-lock.json are committed but node_modules is not.
- Open tsconfig.server.json and explain NodeNext, erasableSyntaxOnly, verbatimModuleSyntax, noEmit, and strict.
- Confirm that the complete typecheck script preserves the existing client checks and adds the server configuration.
- Confirm that the build command runs the complete type check before Vite creates client assets.
- Explain why dev:server watch behavior does not replace type checking.
- Point to the module that owns Express configuration and the module that owns process configuration and listening.
- Trace GET /api/health through method and path matching, handler execution, status, headers, serialization, and sent bytes.
- Confirm that the exact health body, 200 status, JSON content type, and no-store cache policy match README.
- Confirm that the health response does not include X-Powered-By and state why this is not a complete security claim.
- Explain why process.env.PORT is string or undefined and why a number type cannot validate its content.
- Run the controlled 70000 port value, confirm that no listener starts, and remove the environment variable.
- Distinguish the unmatched-route 404 from the connection failure after the process stops.
- Confirm that the server listens on 127.0.0.1 and has not been exposed to a public or local network interface.
- Run the existing client tests, complete typecheck, lint, and production build after adding the server.
- Confirm that README records the two-process workflow, exact stop action, health evidence, recovery, and current limits.
- Inspect the staged diff, create the focused server-foundation commit, and confirm a clean feature branch.
Explanation: a server is a process that fulfills contracts
Section titled “Explanation: a server is a process that fulfills contracts”Express reduces the low-level work required to use Node’s HTTP facilities. It supplies ordered middleware and route matching plus convenient response methods. The application still owns the public contract and every trust decision.
The health route is intentionally small. It proves the complete path from a network request to a JSON response while avoiding work-item input, mutation, storage, and client integration. This makes the first failure surface narrow:
- No connection: process, host, or port problem.
- 404 response: running process, unmatched method or path.
- Wrong status or body: matched handler or response-contract problem.
- Type diagnostic: source relationship rejected before runtime evidence.
- Client failure: separate browser application problem.
These categories prevent random edits. Identify the failing boundary first.
The separated app and index modules also prepare the next test seam. API tests can exercise the Express application without competing for a port. The normal entry module can still validate real process configuration and open the local listener.
Official references
Section titled “Official references”- Node.js: Running TypeScript natively — supported type stripping, limitations, and the separate type-check responsibility
- Node.js: Process — process information and environment access
- Node.js: HTTP — the low-level request and response facilities beneath Express
- Express 5: Installing — current requirements, dependency installation, TypeScript packages, and server configuration
- Express 5: Basic routing — method and path route matching
- Express 5: API reference — application, request, and response methods
- TypeScript: Modules reference — Node module formats and
NodeNext - MDN: Overview of HTTP — request-response messages, methods, status, and headers
Optional extensions
Section titled “Optional extensions”The health route, configuration check, documentation, and focused commit complete this lesson. Keep an extension in a separate commit or restore the required result before continuing.
Next step or safe stopping point
Section titled “Next step or safe stopping point”The required result is safely paused when the health and unmatched-route evidence match README, the invalid port is restored, no server process remains active, all repository checks pass, the focused server-foundation commit exists, and git status is clean on feature/express-api.
Continue to Develop, validate, and test a JSON API. That lesson will add work-item routes, JSON body parsing, runtime validation, explicit error contracts, and automated API tests without adding a database or authentication system.