How Instatic Validates Data at Untyped Boundaries Using TypeBox
Instatic enforces a strict "validate‑then‑trust" contract where all data entering from HTTP, JSON.parse, plugin manifests, persisted files, or inter‑process communication is validated against a TypeBox schema before downstream code ever sees it.
Instatic's architecture treats every external data source as an untyped boundary. Rather than casting or assuming shapes, the codebase routes all ingress through specialized utilities that validate data using TypeBox schemas. This article examines the five validation entry points, the compiled validator pattern, and how Instatic prevents "as‑cast" shortcuts at every boundary.
The Core Validation Philosophy
Instatic's design documentation explicitly defines five boundary rules covering HTTP traffic, JSON parsing, raw fetch calls, raw req.json() usage, and field casting. According to the TypeBox patterns documentation, no data may cross into the trusted interior without schema validation. This policy resides in docs/reference/typebox-patterns.md and guides all implementation decisions.
The practical result: downstream handlers, React components, and plugin runtimes never receive unchecked data. Every validation failure produces either a typed error or a safe fallback, never an ambiguous shape.
HTTP Boundary: Browser to Server
Client-Side Validation with apiRequest
Every browser‑to‑server call routes through src/core/http/apiClient.ts. The apiRequest function automatically validates success payloads against a supplied TypeBox schema and throws a typed ApiError on failure.
import { apiRequest } from '../core/http/apiClient';
import { UsersResponseSchema } from '../schemas/api';
// Validation happens automatically before the promise resolves
const users = await apiRequest('/admin/api/cms/users', {
schema: UsersResponseSchema
});
The apiRequest utility in src/core/http/apiClient.ts guarantees that UsersResponseSchema is enforced before React state receives the data. Invalid payloads never reach component code.
Server-Side Validation with readValidatedBody
On the server, request bodies are parsed exclusively via readValidatedBody in server/http.ts. This helper parses the raw request, validates with TypeBox, and passes only confirmed‑valid data to route handlers.
import { readValidatedBody } from '../server/http';
import { CreateUserSchema } from '../schemas/api';
export async function createUserRoute(req: Request) {
// Throws early if body fails schema validation
const body = await readValidatedBody(req, CreateUserSchema);
// From here, `body` is trusted to match CreateUserSchema
return createUserInDatabase(body);
}
The server/http.ts implementation ensures downstream handlers never see unchecked data. This creates symmetric validation: client and server both enforce schemas at the HTTP boundary.
JSON Parsing Boundaries
Core Validation Helpers in jsonValidate.ts
The src/core/utils/jsonValidate.ts module provides three primitives for JSON validation at untyped boundaries:
safeParseJson– parses a string and validates it, returning a discriminated union for hard‑error versus soft‑fallback handlingparseJsonWithFallback– "best‑effort" parsing for local storage or optional config, returning a default on any failureparseJsonResponse– parses aResponseand validates it, throwing on schema mismatch
import { safeParseJson, parseJsonWithFallback } from '../core/utils/jsonValidate';
import { SettingsSchema, SiteSchema } from '../schemas/persistence';
// Hard validation: success or error
const result = safeParseJson(rawSiteJson, SiteSchema);
if (result.success) {
// result.data is guaranteed to match SiteSchema
} else {
// Handle validation error explicitly
}
// Soft validation with safe default
const settings = parseJsonWithFallback(
localStorage.getItem('settings'),
SettingsSchema,
/* fallback */ { theme: 'system', fontSize: 16 }
);
// Corrupted JSON silently falls back, never crashes the UI
parseJsonWithFallback is particularly valuable for local storage and optional configuration where corrupted data should degrade gracefully rather than throw.
Persistence Layer Integration
For database JSON columns, src/core/persistence/validate.ts uses safeParseJson to guarantee stored documents conform to canonical schemas before loading into the editor:
import { safeParseJson } from '../core/utils/jsonValidate';
import { SiteSchema } from '../schemas/site';
// Loading from DB JSON column
const site = safeParseJson(rawSiteJson, SiteSchema);
// site.data is trusted; site.error indicates migration or corruption
Server-to-Server and Internal Fetch
Internal service calls use readEnvelope in server code, enforcing the same schema check without a second‑level wrapper:
import { readEnvelope } from '../server/http';
const res = await fetch('https://internal.service/api/data');
const data = await readEnvelope(res, InternalDataSchema);
// Throws on schema mismatch; otherwise data is trusted
This maintains validation consistency even when Instatic services communicate with each other.
Plugin Manifest Validation
Plugin manifests undergo validation before runtime instantiation via parsePluginManifest in the plugin SDK:
import { parsePluginManifest } from '../plugin-sdk/manifest';
const manifest = parsePluginManifest(rawJson);
// manifest is guaranteed to match PluginManifestSchema
// Plugin runtime is only instantiated after validation passes
This prevents malformed or malicious plugin configurations from entering the system.
Compiled Validators for Performance
Instatic avoids recompiling TypeBox schemas on every validation call. The src/core/utils/typeboxCompiler.ts module compiles each schema once and reuses the generated validator:
// In src/core/utils/typeboxCompiler.ts
import { TypeCompiler } from '@sinclair/typebox/compiler';
const compileCache = new Map<string, ReturnType<typeof TypeCompiler.Compile>>();
export function getCompiledValidator<T extends TSchema>(schema: T) {
const key = JSON.stringify(schema); // Simplified; actual implementation uses stable hashing
if (!compileCache.has(key)) {
compileCache.set(key, TypeCompiler.Compile(schema));
}
return compileCache.get(key)!;
}
This compile‑once pattern ensures fast runtime checks even under high throughput, keeping validation overhead minimal at every boundary.
Boundary Validation Summary
| Boundary | Entry Point | Source File | Failure Behavior |
|---|---|---|---|
| Browser → Server | apiRequest |
src/core/http/apiClient.ts |
Throws ApiError |
| Server request body | readValidatedBody |
server/http.ts |
Throws validation error |
| Server → Server fetch | readEnvelope |
server/http.ts |
Throws on mismatch |
| Local storage / config | parseJsonWithFallback |
src/core/utils/jsonValidate.ts |
Returns safe default |
| Persistence (DB JSON) | safeParseJson |
src/core/persistence/validate.ts |
Returns discriminated union |
| Plugin manifests | parsePluginManifest |
Plugin SDK | Prevents instantiation |
All paths converge on the compiled validators from src/core/utils/typeboxCompiler.ts, ensuring consistent performance characteristics across the codebase.
Integration with Type System
TypeBox schemas serve double duty: they produce both runtime validators and static TypeScript types. Instatic leverages this to eliminate drift between validation logic and type annotations. When a schema changes, the compiler enforces updates at all usage sites.
This is implemented according to the TypeBox patterns documentation, which mandates that no boundary may use raw as casting or unvalidated JSON.parse calls.
Summary
- Five boundary rules documented in
docs/reference/typebox-patterns.mdgovern all untyped data entry apiRequestandreadValidatedBodyenforce symmetric HTTP validation between client and serverjsonValidate.tsprimitives (safeParseJson,parseJsonWithFallback,parseJsonResponse) handle JSON parsing boundaries with appropriate failure modestypeboxCompiler.tscompiles schemas once for performant repeated validation- Plugin manifests, persistence layers, and internal service calls all route through the same validation pipeline
- No "as‑cast" shortcuts exist in the codebase; every boundary validates before trusting
Frequently Asked Questions
What happens when TypeBox validation fails at an HTTP boundary?
The apiRequest client utility throws a typed ApiError containing validation details, while readValidatedBody on the server throws a validation error before the route handler executes. Both prevent unchecked data from reaching application logic. As implemented in CoreBunch/Instatic, these errors include the specific schema violations for debugging.
Why does Instatic use TypeBox instead of Zod or Yup?
TypeBox provides both runtime validation and static TypeScript type inference from a single schema definition. The src/core/utils/typeboxCompiler.ts module additionally leverages TypeBox's compilation API for optimized validators. This dual purpose reduces maintenance overhead and prevents type‑validation drift that can occur with separate systems.
How does Instatic handle corrupted data in localStorage?
The parseJsonWithFallback helper in src/core/utils/jsonValidate.ts returns a configured default value when JSON is malformed or fails schema validation. This "best‑effort" pattern ensures UI stability—corrupted settings silently reset to safe defaults rather than crashing components.
Is there performance overhead from validating every boundary?
Minimal. The typeboxCompiler.ts utility compiles each TypeBox schema once and caches the result, making subsequent validations fast native code checks. According to the Instatic source, this compile‑once pattern keeps validation overhead negligible even for high‑frequency operations like real‑time sync.
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 →