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

> Explore Instatic TypeBox validation patterns for secure untyped boundaries. Learn how Instatic validates external data against TypeBox schemas for robust type safety.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: deep-dive
- Published: 2026-07-28

---

**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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxHelpers.ts) | Core TypeBox utilities including `parseValue`, `withFallback`, and `filterArray` |
| [`src/core/utils/typeboxCompiler.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxCompiler.ts) | Cached compiled validators (`compiled`, `compiledCheck`, `compiledDecode`) for performance-critical paths |
| [`src/core/utils/jsonValidate.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/jsonValidate.ts) | JSON-specific parsing helpers: `safeParseJson`, `parseJsonWithFallback`, `parseJsonResponse` |
| [`src/core/http/apiClient.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts) | Canonical HTTP client (`apiRequest`, `apiBlobRequest`, `readEnvelope`) with automatic response validation |
| [`server/http.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/http.ts) | Server-side request validation via `readValidatedBody` |
| [`src/core/persistence/validate.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/validate.ts) | Persistence layer validators for site data, pages, and visual components |
| [`src/core/plugins/manifest.ts`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts). The `apiRequest` function automatically serializes bodies, sends credentials, and validates responses against provided schemas, throwing `ApiError` on validation failures.

```typescript
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.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxCompiler.ts) module provides `compiledCheck` and `compiledDecode` functions that cache compiled validation functions.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/plugins/manifest.ts) file provides `parsePluginManifest`, which validates manifest files against `PluginManifestSchema` before the plugin system initializes the extension.

```typescript
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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/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`](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts) (`apiRequest`, `apiBlobRequest`), while server-side request body validation lives in [`server/http.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/http.ts) (`readValidatedBody`). JSON-specific utilities are found in [`src/core/utils/jsonValidate.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/jsonValidate.ts), and the core TypeBox helpers are in [`src/core/utils/typeboxHelpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxHelpers.ts).