Instatic TypeBox Validation Patterns: A Complete Guide to Safe Untyped Boundaries

Instatic uses a strict "validate-then-trust" pattern where all external data—HTTP requests, JSON.parse results, localStorage, and plugin manifests—is validated against TypeBox schemas before entering the type-safe core, utilizing hard boundaries that throw on failure and soft boundaries that fallback to defaults.

Every interaction with data originating outside Instatic's type-checked code is guarded by a comprehensive TypeBox-driven validation layer. In the CoreBunch/Instatic repository, schemas defined with Type from @sinclair/typebox serve as the single source of truth, with TypeScript types derived via Static<typeof Schema> rather than parallel interface definitions. This approach eliminates unsafe type assertions and ensures that invalid data is caught at system boundaries before it can propagate into business logic.

Core Architecture: The Validate-Then-Trust Principle

The foundation of Instatic's TypeBox validation patterns rests on a simple rule: validate-then-trust. All data crossing from untyped environments (network I/O, browser storage, file system) into the application core must pass through a schema validator first.

In src/core/utils/typeboxHelpers.ts, the team defines schemas using the Type builder and extracts TypeScript types using Static<typeof Schema>. This ensures zero divergence between runtime validation and compile-time type checking. The codebase explicitly forbids direct casts like await res.json() as Foo or JSON.parse(...) as Foo, enforcing validation through canonical helper functions instead.

Hard vs Soft Boundaries

Instatic distinguishes between two validation strategies based on the criticality of the data:

Hard Boundaries

Hard boundaries treat invalid data as fatal errors. These are used for request bodies, required configuration files, and API responses where malformed data indicates a programming error or security issue.

The readValidatedBody function in server/http.ts exemplifies this pattern—it parses incoming request bodies and throws if validation against a TypeBox schema fails. Similarly, the HTTP client in src/core/http/apiClient.ts throws a unified ApiError when response validation fails.

Soft Boundaries

Soft boundaries provide graceful degradation for optional or tolerant data sources like localStorage, user preferences, or cached state. Instead of throwing, these helpers return default values when validation fails.

The parseJsonWithFallback function in src/core/utils/jsonValidate.ts implements this pattern, allowing the UI to remain stable even when stored data becomes corrupted or schema versions drift.

Validation Helper Files and Responsibilities

Helper File Primary Responsibility
src/core/utils/typeboxHelpers.ts Core TypeBox utilities including parseValue, withFallback, and filterArray
src/core/utils/typeboxCompiler.ts Cached compiled validators (compiled, compiledCheck, compiledDecode) for performance-critical paths
src/core/utils/jsonValidate.ts JSON-specific parsing helpers: safeParseJson, parseJsonWithFallback, parseJsonResponse
src/core/http/apiClient.ts Canonical HTTP client (apiRequest, apiBlobRequest, readEnvelope) with automatic response validation
server/http.ts Server-side request validation via readValidatedBody
src/core/persistence/validate.ts Persistence layer validators for site data, pages, and visual components
src/core/plugins/manifest.ts Plugin manifest JSON validation

Validating HTTP Requests and Responses

Server-Side Request Validation

For incoming HTTP requests, Instatic uses readValidatedBody to enforce schemas at the edge. This function, defined in server/http.ts, validates request bodies against TypeBox schemas and returns null or throws depending on the implementation, allowing handlers to respond with appropriate error codes.

import { Type } from '@core/utils/typeboxHelpers';
import { readValidatedBody, badRequest, jsonResponse } from '../http';

const CreatePostSchema = Type.Object({
  title: Type.String({ minLength: 1, maxLength: 200 }),
  body:  Type.String(),
});

export async function createPostHandler(req: Request) {
  const body = await readValidatedBody(req, CreatePostSchema);
  if (!body) return badRequest('Invalid request body');

  // `body` is now typed as Static<typeof CreatePostSchema>
  return jsonResponse({ ok: true });
}

Client-Side API Validation

On the client, all browser-to-server calls flow through src/core/http/apiClient.ts. The apiRequest function automatically serializes bodies, sends credentials, and validates responses against provided schemas, throwing ApiError on validation failures.

import { apiRequest } from '@core/http';
import { PostsResponseSchema } from '@/core/persistence/responseSchemas';

const posts = await apiRequest('/admin/api/cms/posts', {
  schema: PostsResponseSchema,
});
// `posts` is typed as Static<typeof PostsResponseSchema>

Handling JSON and Persistence Boundaries

Tolerant LocalStorage Parsing

For browser storage boundaries, Instatic uses soft validation via parseJsonWithFallback. This helper attempts to parse and validate JSON against a schema, returning a default value if parsing fails or the data doesn't match the expected shape.

import { parseJsonWithFallback } from '@core/utils/jsonValidate';
import { EditorPreferencesSchema } from '@/core/persistence/schemas';
import { DEFAULT_PREFERENCES } from '@/ui/constants';

const prefs = parseJsonWithFallback(
  localStorage.getItem('editorPrefs') ?? '',
  EditorPreferencesSchema,
  DEFAULT_PREFERENCES,
);
// `prefs` is guaranteed to conform to the schema; corrupted data silently falls back.

Persisted Site Data Validation

The src/core/persistence/validate.ts file contains validators like validateSite and validatePages that check persisted JSON data when loading sites from disk or external storage. These use the same TypeBox helpers to ensure data integrity across sessions.

Performance Optimization with Compiled Validators

For hot paths involving bulk data or high-frequency validation (such as plugin RPC payloads or data grid rows), Instatic avoids the overhead of repeated Value.Check calls by using compiled validators. The src/core/utils/typeboxCompiler.ts module provides compiledCheck and compiledDecode functions that cache compiled validation functions.

import { compiledCheck, compiledDecode } from '@core/utils/typeboxCompiler';
import { DataRowSchema } from '@/core/persistence/schemas';

if (!compiledCheck(DataRowSchema, rawRow)) {
  throw new Error('Invalid data row');
}
const row = compiledDecode(DataRowSchema, rawRow);

This compilation step is particularly important in the publisher and rendering pipelines where thousands of data rows may require validation during static site generation.

Plugin Manifest and Specialized Boundaries

Plugin manifests represent another untyped boundary where external JSON must be validated before loading. The src/core/plugins/manifest.ts file provides parsePluginManifest, which validates manifest files against PluginManifestSchema before the plugin system initializes the extension.

import { parsePluginManifest } from '@core/plugins/manifest';

const manifestText = await fetch('/plugins/my-plugin/manifest.json')
  .then(r => r.text());

const manifest = parsePluginManifest(manifestText); 
// validates against PluginManifestSchema before returning typed data

Enforcement and Forbidden Patterns

Instatic actively prevents unsafe patterns through architectural gate tests in src/__tests__/architecture/boundary-validation.test.ts. These tests ensure that no prohibited casts or raw fetches slip into the codebase.

Forbidden patterns include:

  • Direct type assertions: await res.json() as Foo
  • Ad-hoc shape checking without schema validation
  • Raw JSON.parse without subsequent validation

All validation must flow through the canonical helpers listed above, ensuring that the "validate-then-trust" invariant holds across the entire codebase.

Summary

  • Validate-then-trust is the core rule: all external data passes through TypeBox schemas before entering typed code.
  • Hard boundaries (using readValidatedBody, apiRequest) throw on validation failure for critical data.
  • Soft boundaries (using parseJsonWithFallback) gracefully degrade to defaults for optional data like localStorage.
  • Compiled validators in typeboxCompiler.ts optimize performance for high-frequency validation scenarios.
  • Unified error handling uses ApiError for client failures and typed errors like SiteValidationError for persistence issues.
  • Architectural tests enforce the boundary patterns, prohibiting direct type casts and unchecked JSON parsing.

Frequently Asked Questions

What is the difference between hard and soft boundaries in Instatic?

Hard boundaries treat validation failures as errors that halt execution, typically used for HTTP request bodies and required configuration files via functions like readValidatedBody. Soft boundaries provide fallback values when validation fails, used for tolerant data sources like localStorage or optional user preferences via parseJsonWithFallback, ensuring UI stability despite corrupted data.

How does Instatic handle TypeBox validation performance in hot paths?

Instatic uses a cached compiler system located in src/core/utils/typeboxCompiler.ts that provides compiledCheck and compiledDecode functions. These compile TypeBox schemas once and reuse the compiled validation functions, avoiding the overhead of re-compiling schemas on every call during bulk operations like processing data rows or plugin RPC messages.

Why does Instatic prohibit direct type assertions like as Foo?

Direct type assertions bypass runtime validation, creating a gap between the assumed TypeScript type and the actual runtime value. Instatic's architecture requires that all data from untyped sources (HTTP, storage, JSON) be validated against TypeBox schemas first, ensuring that the TypeScript type accurately reflects the runtime shape and preventing runtime errors from propagating into business logic.

Where are the canonical HTTP validation helpers located in the Instatic codebase?

The client-side HTTP validation is centralized in src/core/http/apiClient.ts (apiRequest, apiBlobRequest), while server-side request body validation lives in server/http.ts (readValidatedBody). JSON-specific utilities are found in src/core/utils/jsonValidate.ts, and the core TypeBox helpers are in src/core/utils/typeboxHelpers.ts.

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 →