TypeBox Validation Patterns at Instatic System Boundaries

Instatic uses TypeBox schemas at every I/O boundary, enforcing a "validate once, trust everywhere" philosophy through canonical helpers like readValidatedBody, apiRequest, and safeParseJson that prevent malformed data from reaching business logic.

Instatic, an open-source content management system maintained by CoreBunch, implements strict TypeBox validation patterns to guard all system boundaries. The repository CoreBunch/Instatic centralizes data integrity checks in reusable utilities, ensuring that every HTTP request, local storage entry, and plugin manifest conforms to expected schemas before any business logic executes.

Server-Side HTTP TypeBox Validation Patterns

The readValidatedBody Pattern

According to the Instatic source code, every incoming HTTP request from the browser passes through readValidatedBody in server/http.ts (lines 47‑58). This generic function accepts a TypeBox schema and validates the request body before any handler logic processes the data.

// server/http.ts
import { type TSchema, type Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

export async function readValidatedBody<T extends TSchema>(
  req: Request, 
  schema: T
): Promise<Static<T>> {
  const body = await req.json();
  
  if (!Value.Check(schema, body)) {
    throw new Error('Request body validation failed');
  }
  
  return body;
}

All CMS handlers—including those in server/cms/users.ts and server/cms/siteDocument.ts—use this pattern to guarantee that route inputs match their TypeBox schemas.

Client-Side HTTP Validation

Response Validation via apiRequest

The client-side HTTP wrapper apiRequest in src/core/http/apiClient.ts (lines 150‑166) validates success responses against TypeBox schemas before returning data to calling components. This function throws a unified ApiError when validation fails.

// src/core/http/apiClient.ts
import { type TSchema, type Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

export class ApiError extends Error {
  constructor(message: string, public status: number) {
    super(message);
  }
}

export async function apiRequest<S extends TSchema>(
  path: string, 
  options: RequestInit & { schema: S }
): Promise<Static<S>> {
  const res = await fetch(path, options);
  const data = await res.json();
  
  if (!res.ok) throw new ApiError('Request failed', res.status);
  if (!Value.Check(options.schema, data)) {
    throw new ApiError('Response validation failed', 500);
  }
  
  return data;
}

Envelope Validation with readEnvelope

When handling { data, error } envelope responses from the CMS persistence layer, readEnvelope in src/core/persistence/cmsTransfer.ts (lines 11‑22) validates the JSON payload against a supplied TypeBox schema while checking the res.ok status.

// src/core/persistence/cmsTransfer.ts
import { type TSchema, type Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

export async function readEnvelope<T extends TSchema>(
  res: Response,
  schema: T,
  fallbackMsg: string
): Promise<Static<T>> {
  if (!res.ok) throw new Error(fallbackMsg);
  
  const json = await res.json();
  if (!Value.Check(schema, json.data)) {
    throw new Error('Envelope data validation failed');
  }
  
  return json.data;
}

Low-Level JSON TypeBox Validation Utilities

parseJsonResponse and jsonValidate

The foundational parseJsonResponse and jsonValidate utilities in src/core/utils/jsonValidate.ts (lines 2‑30) serve as building blocks for higher-level validators. These functions parse response bodies and execute TypeBox validators directly.

// src/core/utils/jsonValidate.ts
import { Value } from '@sinclair/typebox/value';
import { type TSchema, type Static } from '@sinclair/typebox';

export async function parseJsonResponse<T extends TSchema>(
  res: Response, 
  schema: T
): Promise<Static<T>> {
  const data = await res.json();
  
  if (!Value.Check(schema, data)) {
    throw new Error('JSON validation failed against TypeBox schema');
  }
  
  return data;
}

Safe Local Storage Parsing

For client-side persistence boundaries including localStorage and IndexedDB, safeParseJson and parseJsonWithFallback in src/core/utils/jsonValidate.ts (lines 21‑35) provide corruption-resistant validation with fallback defaults. These functions guard clipboard payloads and spotlight history against corrupted storage data.

// src/core/utils/jsonValidate.ts
import { Value } from '@sinclair/typebox/value';

export function safeParseJson<T extends TSchema>(
  raw: string | null, 
  schema: T
): Static<T> | null {
  if (!raw) return null;
  
  try {
    const parsed = JSON.parse(raw);
    return Value.Check(schema, parsed) ? parsed : null;
  } catch {
    return null;
  }
}

export function parseJsonWithFallback<T extends TSchema>(
  raw: string | null,
  schema: T,
  defaultValue: Static<T>
): Static<T> {
  return safeParseJson(raw, schema) ?? defaultValue;
}

Domain-Specific TypeBox Validation Patterns

Plugin Manifest Parsing

When loading plugin bundles, parsePluginManifest in src/core/plugins/manifest.ts (lines 70‑90) validates the plugin.json file against a strict TypeBox schema defined in the same file. This ensures the manifest conforms to the expected shape before the system loads the plugin.

// src/core/plugins/manifest.ts
import { Type, type Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

const PluginManifestSchema = Type.Object({
  name: Type.String(),
  version: Type.String(),
  entry: Type.String()
});

export function parsePluginManifest(raw: unknown): Static<typeof PluginManifestSchema> {
  if (!Value.Check(PluginManifestSchema, raw)) {
    throw new Error('Invalid plugin manifest format');
  }
  return raw;
}

Site Document Validation

The site-runtime loader uses validateSite in src/core/persistence/validate.ts (lines 7‑12) to validate entire site JSON documents against TypeBox schemas before editors or publishers interact with the data.

// src/core/persistence/validate.ts
import { Type, type Static } from '@sinclair/typebox';
import { Value } from '@sinclair/typebox/value';

const SiteDocumentSchema = Type.Object({
  id: Type.String(),
  components: Type.Array(Type.Any())
});

export function validateSite(document: unknown): Static<typeof SiteDocumentSchema> {
  if (!Value.Check(SiteDocumentSchema, document)) {
    throw new Error('Site document validation failed');
  }
  return document;
}

Schema Definitions and Error Handling

Centralized Schema Definitions

As implemented in CoreBunch/Instatic, every module that persists data ships its own TypeBox schema as the single source of truth. For example, src/core/visualComponents/schemas.ts (lines 2‑10) exports schemas for visual components, layouts, fonts, and data tables.

// src/core/visualComponents/schemas.ts
import { Type } from '@sinclair/typebox';

export const VisualComponentSchema = Type.Object({
  id: Type.String(),
  type: Type.String(),
  props: Type.Record(Type.String(), Type.Any())
});

Error Message Extraction

The getErrorMessage utility in src/core/utils/errorMessage.ts (lines 1‑12) wraps ApiError instances and TypeBox validation failures into user-friendly strings for the global toast notification system.

// src/core/utils/errorMessage.ts
export function getErrorMessage(err: unknown, fallback: string): string {
  if (err instanceof Error) return err.message;
  if (typeof err === 'string') return err;
  return fallback;
}

Summary

  • Single-responsibility validators: Every I/O boundary uses dedicated helpers like readValidatedBody, apiRequest, and safeParseJson that own TypeBox validation exclusively, as defined in server/http.ts and src/core/utils/jsonValidate.ts.
  • Schema-driven architecture: All validation accepts TSchema arguments from TypeBox, eliminating ad-hoc type casting and manual property checks throughout the CoreBunch/Instatic codebase.
  • Centralized error handling: Validation failures propagate as ApiError on the client or typed server errors, then render via global toast notifications using getErrorMessage.
  • Consistent naming conventions: Helpers follow clear patterns (read*, parse*, validate*) making validation boundaries obvious in src/core/persistence/cmsTransfer.ts and related modules.
  • Defense in depth: From HTTP requests in server/http.ts to local storage entries and plugin manifests in src/core/plugins/manifest.ts, every external input validates exactly once before entering the trusted system core.

Frequently Asked Questions

What validation library does Instatic use at system boundaries?

Instatic uses TypeBox (@sinclair/typebox) as its primary validation library. Every system boundary—from HTTP requests in server/http.ts to local storage in src/core/utils/jsonValidate.ts—uses TypeBox schemas as the single source of truth for data integrity.

How does Instatic handle validation errors?

Validation errors are converted into unified ApiError instances on the client side or thrown as typed errors on the server. The getErrorMessage utility in src/core/utils/errorMessage.ts extracts human-readable messages from these errors for display in the global toast notification system.

Why does Instatic validate data at every system boundary?

The codebase follows a "validate once, trust everywhere" philosophy. By enforcing TypeBox schemas at every I/O boundary—whether browser-to-server HTTP, plugin manifests in src/core/plugins/manifest.ts, or local storage—Instatic guarantees that malformed or corrupted data never reaches business logic, preventing runtime errors and data corruption.

Where are TypeBox schemas defined in the Instatic repository?

TypeBox schemas are defined adjacent to the data they describe. For example, visual component schemas live in src/core/visualComponents/schemas.ts, site document schemas in src/core/persistence/validate.ts, and plugin manifest schemas in src/core/plugins/manifest.ts. This co-location ensures schemas remain synchronized with their usage.

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 →