How TypeBox Validation Works at HTTP and Persistence Boundaries in Instatic

Instatic enforces type safety by validating every HTTP payload and persisted JSON document against compiled TypeBox schemas at the system boundary, ensuring only statically-typed data enters the application core.

The CoreBunch/Instatic repository implements a strict "validate once at the boundary" architecture. By leveraging TypeBox schemas as the single source of truth, the codebase guarantees that untyped data from network requests and storage layers is parsed and validated before reaching business logic.

HTTP Request Boundary Validation

Incoming HTTP requests represent the first untrusted boundary. Instatic uses a centralized helper to parse and validate request bodies against strict TypeBox schemas before they reach route handlers.

The readValidatedBody Helper

Located in server/http.ts (around line 47), the readValidatedBody function reads the raw request body, parses the JSON, and validates it against a provided TSchema. If validation fails, it throws an ApiError containing the HTTP status code and formatted error messages.

// server/http.ts
import { TSchema, Static } from '@sinclair/typebox';
import { parseValue } from '../src/core/utils/typeboxHelpers';

export async function readValidatedBody<T extends TSchema>(
  req: Request,
  schema: T,
  opts?: { maxBytes?: number }
): Promise<Static<T>> {
  const raw = await req.text({ maxBytes: opts?.maxBytes });
  const parsed = JSON.parse(raw);
  // Validates against compiled TypeBox schema
  const value = parseValue(parsed, schema);
  return value; // Returns Static<T> for type-safe usage
}

This pattern ensures that route handlers receive fully typed data. For example, a user creation handler imports the schema and receives a typed object without manual casting.

HTTP Response Boundary Validation

Outbound HTTP responses from internal API clients must also conform to expected shapes. Instatic wraps the native fetch API with validators that check response payloads before resolving.

The apiRequest Wrapper

The apiRequest function in src/core/http/apiClient.ts sends HTTP requests and validates successful response bodies against TypeBox schemas. It unwraps error envelopes for non-OK status codes and applies the same validation pipeline used for incoming requests.

// src/core/http/apiClient.ts
import { TSchema, Static } from '@sinclair/typebox';
import { parseValue } from '../utils/typeboxHelpers';
import { ApiError } from '../utils/errors';

export async function apiRequest<T extends TSchema>(
  path: string,
  { schema, ...init }: ApiRequestOptions<T>
): Promise<Static<T>> {
  const res = await fetch(path, init);
  if (!res.ok) {
    throw new ApiError(res.status, await responseErrorMessage(res));
  }
  const json = await res.json();
  // Validates response body matches expected schema
  return parseValue(json, schema);
}

This symmetric validation—enforcing schemas on both request bodies and response payloads—prevents type mismatches between the frontend and backend services.

Persistence Boundary Validation

Data retrieved from databases or file systems is treated as untyped until validated. The persistence layer uses shared utilities to parse stored JSON and verify it against domain-specific schemas.

Parsing Stored Data with parseValue

The parseValue function in src/core/utils/typeboxHelpers.ts serves as the primary entry point for persistence validation. It retrieves a compiled validator (via compiledCheck) and throws descriptive errors for malformed data.

// src/core/utils/typeboxHelpers.ts
import { TSchema, Static } from '@sinclair/typebox';
import { compiledCheck } from './typeboxCompiler';

export function parseValue<T extends TSchema>(value: unknown, schema: T): Static<T> {
  const check = compiledCheck(schema);
  if (!check.Check(value)) {
    const errors = formatValueErrors(check.Errors);
    throw new Error(errors);
  }
  return value as Static<T>;
}

Persistence modules throughout src/core/persistence/ invoke this helper when hydrating documents from storage, ensuring that database migrations or manual edits cannot corrupt the runtime type system.

The Validation Pipeline

Instatic organizes its validation logic into three distinct phases to maximize performance and reusability:

  1. Schema Definition: Developers define data structures using TypeBox's Type.Object, Type.String, and other constructors, creating TSchema instances that serve as the single source of truth.

  2. Compilation: The compiledCheck function in src/core/utils/typeboxCompiler.ts uses @sinclair/typebox/compiler to generate optimized validation functions. These compiled checkers are cached and reused across requests, avoiding the overhead of interpreting schemas at runtime.

  3. Execution and Error Formatting: When data crosses a boundary, parseValue executes the compiled checker. On failure, formatValueErrors transforms the iterator of TypeBox Error objects into human-readable strings suitable for UI toast notifications via pushToast({ kind: 'error', ... }).

This pipeline applies uniformly across HTTP handlers, API clients, and persistence modules, maintaining consistent error messages and validation logic throughout the stack.

Summary

  • Single Source of Truth: TypeBox schemas defined in server/http.ts and src/core/persistence/ provide the canonical types for all data crossing system boundaries.
  • Compiled Validators: src/core/utils/typeboxCompiler.ts generates fast, reusable validation functions using TypeCompiler to minimize runtime overhead.
  • HTTP Enforcement: readValidatedBody validates incoming requests while apiRequest validates outgoing responses,both utilizing parseValue from src/core/utils/typeboxHelpers.ts.
  • Persistence Safety: Stored JSON is never trusted until passed through parseValue, preventing type corruption from external data stores.
  • Consistent Errors: Validation failures generate standardized error messages via formatValueErrors, enabling uniform error handling across HTTP and persistence layers.

Frequently Asked Questions

What is TypeBox and why does Instatic use it for validation?

TypeBox is a JSON Schema type builder library that creates runtime-validated TypeScript types. Instatic uses it because it provides zero-cost abstractions—schemas compile to efficient validation code while simultaneously providing static TypeScript types, eliminating the need to maintain separate .d.ts files and validation logic.

How does Instatic handle validation errors at the HTTP boundary?

When readValidatedBody or apiRequest encounters invalid data, they throw an ApiError containing the HTTP status code and a formatted message from formatValueErrors. Global error handlers catch these exceptions and return standardized JSON error responses to the client, often triggering toast notifications in the UI.

Where are TypeBox schemas defined in the Instatic codebase?

Schemas are co-located with their usage. HTTP route handlers define request body schemas in files like server/handlers/cms/users.ts, while persistence schemas reside in src/core/persistence/responseSchemas.ts and similar files. This colocation ensures that changes to data structures automatically update both the TypeScript types and runtime validators.

Can validation fail silently in Instatic's persistence layer?

No. Unlike optional runtime type checking, Instatic's persistence layer requires successful validation before returning data. The parseValue function throws a hard error if stored data does not match the schema, forcing developers to address data corruption immediately rather than propagating any types through the application.

Have a question about this repo?

These articles cover the highlights, but your codebase questions are specific. Give your agent direct access to the source. Share this with your agent to get started:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →