Skip to content

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

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

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

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

  1. The transport is injected, so the example does not contact example.com.
  2. A successful HTTP status is checked before parsing.
  3. parseTask constructs a fresh object containing only accepted fields.

Expected result

Validate input
Invalid task payload

4. 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.

Read the related official TypeScript documentation ↗