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.parsewithout 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 likelocalStorage. - Compiled validators in
typeboxCompiler.tsoptimize performance for high-frequency validation scenarios. - Unified error handling uses
ApiErrorfor client failures and typed errors likeSiteValidationErrorfor 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:
curl -s "https://instagit.com/install.md" Maintain an open-source project? Get it listed too →