Make network data safe

Static types protect authored TypeScript. Runtime schemas protect values that arrive after compilation.

Treat external data as unknown

response.json() can return any JSON value. A server bug, proxy, stale deployment, or unexpected HTML error body can violate your TypeScript type. Assigning the result directly to Question[] only silences the compiler.

const body: unknown = await response.json();

The unknown type forces code to prove the value before accessing fields.

Define one executable contract

ArkType schemas are JavaScript values that validate at runtime. Their inferred types keep the TypeScript definition synchronized.

import { type } from "arktype";

export const Category = type(
  "'science' | 'history' | 'geography'",
);
export type Category = typeof Category.infer;

export const GetQuestionsInput = type({
  category: Category,
  count: "1 <= number.integer <= 50",
  "difficulty?": "'easy' | 'medium' | 'hard'",
  "includeAnswer?": "boolean",
});
export type GetQuestionsInput = typeof GetQuestionsInput.infer;

The schema and type use the same name because TypeScript keeps value names and type names in separate namespaces. GetQuestionsInput(data) runs; GetQuestionsInput in a type position exists only for the compiler.

Share contracts only when code must agree at runtime

The API validates incoming requests. The SDK validates API responses. A contracts package gives both sides the same runtime schemas instead of copying static interfaces.

The package name contracts has no special behavior. It communicates responsibility. shared-types is less accurate because this package contains runtime JavaScript too.

Validate before returning data

import { ArkErrors } from "arktype";
import { GetQuestionsOutput } from "@your-scope/quiz-contracts";

const body: unknown = await response.json();
const output = GetQuestionsOutput(body);

if (output instanceof ArkErrors) {
  throw new Error("Invalid quiz API response");
}

return output;

After the error check, ArkType narrows output to the validated output type. The SDK returns data that passed the runtime contract.

Separate failure categories

Network failure
fetch rejects before an HTTP response exists. Preserve the original error as cause.
HTTP error
The server responds with a non-success status. Validate its documented error body before constructing QuizApiError.
Invalid success response
The server reports success, but the JSON violates the output schema. Reject it instead of returning a lie.
Malformed JSON
response.json() rejects. Decide whether to expose one stable SDK error or preserve the parser error as a cause.

A broad try...catch can translate unexpected transport and parsing failures. It should not hide expected response.ok handling or discard useful causes.

Keep one explicit install for consumers

{
  "dependencies": {
    "@your-scope/quiz-contracts": "workspace:*",
    "arktype": "^2.2.3"
  }
}

The SDK's compiled JavaScript imports both packages, so both are runtime dependencies. pnpm installs them transitively. Consumers still run one command:

pnpm add @your-scope/quiz-sdk

Re-export useful contract types from the SDK. Users should not install contracts directly unless they are building a compatible API or tooling.

Related references

Check your understanding

  1. Why is checking only that a response is JSON insufficient?

    Show answer

    Valid JSON says nothing about required fields, allowed values, array lengths, or forbidden answer fields. A runtime schema checks the domain shape.

  2. Why is ArkType a runtime dependency instead of a development dependency?

    Show answer

    The published JavaScript imports and executes ArkType when users call the SDK. Consumers need it at runtime.