APPLICATION DEVELOPMENT · LESSON 17 OF 24
Fetch and runtime validation
Turn unknown JSON into trusted application data.
- fetch
- Response
- unknown
- validation
Your runnable checkpoint
Extract into a new practice folder. Each download includes complete source, package.json, tsconfig.json and an offline lesson. Use Node.js 22 or 24. Each checkpoint is independent.
Install with npm install. Run npm run check, then npm run build. Terminal lessons use npm start; browser lessons use npm run serve and http://127.0.0.1:4173.
1. Understand the concepts
An HTTP success and a valid application payload are different checks. fetch normally rejects for network failures, but a 404 still produces a Response. Check response.ok before reading and accepting the body.
A TypeScript interface does not validate JSON. Treat response.json() as unknown, then inspect every field needed by your application. A schema library can make this easier for larger contracts, but the validation must still execute at runtime.
This checkpoint injects a transport function. Its local transport returns a real Response with deterministic JSON, making the example runnable offline. A real call can pass fetch and a trusted URL; configure authentication, timeouts and CORS according to that API.
Keep HTTP transport, validation and display separate. The parser below verifies a positive integer id and a non-empty title. Error handling can then distinguish an unsuccessful status from malformed data without pretending a cast made the data safe.
2. Build and run
These are complete files. Preserve the paths shown below, including the src directory. The package and compiler configuration are included at the end.
src/main.ts
type Task = { id: number; title: string };
function parseTask(value: unknown): Task {
if (typeof value !== "object" || value === null
|| !("id" in value) || typeof value.id !== "number" || !Number.isInteger(value.id) || value.id <= 0
|| !("title" in value) || typeof value.title !== "string" || !value.title.trim()) {
throw new Error("Invalid task payload");
}
return { id: value.id, title: value.title.trim() };
}
type Transport = (url: string) => Promise<Response>;
async function loadTask(url: string, transport: Transport): Promise<Task> {
const response = await transport(url);
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const data: unknown = await response.json();
return parseTask(data);
}
const fixture: Transport = async () => new Response(JSON.stringify({ id: 1, title: "Validate input" }), {
status: 200, headers: { "Content-Type": "application/json" }
});
console.log((await loadTask("https://example.com/tasks/1", fixture)).title);
try { parseTask({ id: "wrong", title: "Oops" }); }
catch (error: unknown) { console.log(error instanceof Error ? error.message : "Unknown error"); }
package.json
{
"name": "edupoly-typescript-fetch-and-runtime-validation",
"private": true,
"version": "1.0.0",
"type": "module",
"scripts": {
"check": "tsc --noEmit",
"build": "tsc",
"start": "node dist/main.js"
},
"devDependencies": {
"typescript": "5.9.3"
}
}
tsconfig.json
{
"compilerOptions": {
"target": "ES2022",
"module": "NodeNext",
"moduleResolution": "NodeNext",
"lib": [
"ES2022",
"DOM"
],
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noEmitOnError": true,
"verbatimModuleSyntax": true,
"rootDir": "src",
"outDir": "dist",
"sourceMap": true,
"types": [],
"moduleDetection": "force"
},
"include": [
"src/**/*"
]
}
3. Trace the code and check the result
- The transport is injected, so the example does not contact example.com.
- A successful HTTP status is checked before parsing.
- parseTask constructs a fresh object containing only accepted fields.
Expected result
Validate input
Invalid task payload4. Practice independently
Make the fixture return HTTP 404 and handle the rejected loadTask call.
Show the explanation
Return new Response("Not found", { status: 404 }); wrap await loadTask in try/catch. Expect HTTP 404 rather than a JSON parse error.
When something goes wrong
If tsc is missing, run npm install in the folder containing package.json. If the output looks old, rebuild before running. Read the first compiler diagnostic and check the indicated field or argument. For module errors, preserve the .js extensions in imports. Browser examples must run through the included local server, not directly as TypeScript files.
