# How TypeBox Validation Works at HTTP and Persistence Boundaries in Instatic

> Discover how Instatic uses TypeBox validation to ensure type safety for HTTP payloads and persisted JSON. Learn how statically-typed data flows into your application core.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: internals
- Published: 2026-07-29

---

**Instatic enforces type safety by validating every HTTP payload and persisted JSON document against compiled TypeBox schemas at the system boundary, ensuring only statically-typed data enters the application core.**

The CoreBunch/Instatic repository implements a strict "validate once at the boundary" architecture. By leveraging TypeBox schemas as the single source of truth, the codebase guarantees that untyped data from network requests and storage layers is parsed and validated before reaching business logic.

## HTTP Request Boundary Validation

Incoming HTTP requests represent the first untrusted boundary. Instatic uses a centralized helper to parse and validate request bodies against strict TypeBox schemas before they reach route handlers.

### The `readValidatedBody` Helper

Located in [`server/http.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/http.ts) (around line 47), the `readValidatedBody` function reads the raw request body, parses the JSON, and validates it against a provided `TSchema`. If validation fails, it throws an `ApiError` containing the HTTP status code and formatted error messages.

```typescript
// server/http.ts
import { TSchema, Static } from '@sinclair/typebox';
import { parseValue } from '../src/core/utils/typeboxHelpers';

export async function readValidatedBody<T extends TSchema>(
  req: Request,
  schema: T,
  opts?: { maxBytes?: number }
): Promise<Static<T>> {
  const raw = await req.text({ maxBytes: opts?.maxBytes });
  const parsed = JSON.parse(raw);
  // Validates against compiled TypeBox schema
  const value = parseValue(parsed, schema);
  return value; // Returns Static<T> for type-safe usage
}

```

This pattern ensures that route handlers receive fully typed data. For example, a user creation handler imports the schema and receives a typed object without manual casting.

## HTTP Response Boundary Validation

Outbound HTTP responses from internal API clients must also conform to expected shapes. Instatic wraps the native `fetch` API with validators that check response payloads before resolving.

### The `apiRequest` Wrapper

The `apiRequest` function in [`src/core/http/apiClient.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts) sends HTTP requests and validates successful response bodies against TypeBox schemas. It unwraps error envelopes for non-OK status codes and applies the same validation pipeline used for incoming requests.

```typescript
// src/core/http/apiClient.ts
import { TSchema, Static } from '@sinclair/typebox';
import { parseValue } from '../utils/typeboxHelpers';
import { ApiError } from '../utils/errors';

export async function apiRequest<T extends TSchema>(
  path: string,
  { schema, ...init }: ApiRequestOptions<T>
): Promise<Static<T>> {
  const res = await fetch(path, init);
  if (!res.ok) {
    throw new ApiError(res.status, await responseErrorMessage(res));
  }
  const json = await res.json();
  // Validates response body matches expected schema
  return parseValue(json, schema);
}

```

This symmetric validation—enforcing schemas on both request bodies and response payloads—prevents type mismatches between the frontend and backend services.

## Persistence Boundary Validation

Data retrieved from databases or file systems is treated as untyped until validated. The persistence layer uses shared utilities to parse stored JSON and verify it against domain-specific schemas.

### Parsing Stored Data with `parseValue`

The `parseValue` function in [`src/core/utils/typeboxHelpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxHelpers.ts) serves as the primary entry point for persistence validation. It retrieves a compiled validator (via `compiledCheck`) and throws descriptive errors for malformed data.

```typescript
// src/core/utils/typeboxHelpers.ts
import { TSchema, Static } from '@sinclair/typebox';
import { compiledCheck } from './typeboxCompiler';

export function parseValue<T extends TSchema>(value: unknown, schema: T): Static<T> {
  const check = compiledCheck(schema);
  if (!check.Check(value)) {
    const errors = formatValueErrors(check.Errors);
    throw new Error(errors);
  }
  return value as Static<T>;
}

```

Persistence modules throughout `src/core/persistence/` invoke this helper when hydrating documents from storage, ensuring that database migrations or manual edits cannot corrupt the runtime type system.

## The Validation Pipeline

Instatic organizes its validation logic into three distinct phases to maximize performance and reusability:

1.  **Schema Definition**: Developers define data structures using TypeBox's `Type.Object`, `Type.String`, and other constructors, creating `TSchema` instances that serve as the single source of truth.

2.  **Compilation**: The `compiledCheck` function in [`src/core/utils/typeboxCompiler.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxCompiler.ts) uses `@sinclair/typebox/compiler` to generate optimized validation functions. These compiled checkers are cached and reused across requests, avoiding the overhead of interpreting schemas at runtime.

3.  **Execution and Error Formatting**: When data crosses a boundary, `parseValue` executes the compiled checker. On failure, `formatValueErrors` transforms the iterator of TypeBox `Error` objects into human-readable strings suitable for UI toast notifications via `pushToast({ kind: 'error', ... })`.

This pipeline applies uniformly across HTTP handlers, API clients, and persistence modules, maintaining consistent error messages and validation logic throughout the stack.

## Summary

-   **Single Source of Truth**: TypeBox schemas defined in [`server/http.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/http.ts) and `src/core/persistence/` provide the canonical types for all data crossing system boundaries.
-   **Compiled Validators**: [`src/core/utils/typeboxCompiler.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxCompiler.ts) generates fast, reusable validation functions using `TypeCompiler` to minimize runtime overhead.
-   **HTTP Enforcement**: `readValidatedBody` validates incoming requests while `apiRequest` validates outgoing responses,both utilizing `parseValue` from [`src/core/utils/typeboxHelpers.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/typeboxHelpers.ts).
-   **Persistence Safety**: Stored JSON is never trusted until passed through `parseValue`, preventing type corruption from external data stores.
-   **Consistent Errors**: Validation failures generate standardized error messages via `formatValueErrors`, enabling uniform error handling across HTTP and persistence layers.

## Frequently Asked Questions

### What is TypeBox and why does Instatic use it for validation?

TypeBox is a JSON Schema type builder library that creates runtime-validated TypeScript types. Instatic uses it because it provides **zero-cost abstractions**—schemas compile to efficient validation code while simultaneously providing static TypeScript types, eliminating the need to maintain separate [`.d.ts`](https://github.com/CoreBunch/Instatic/blob/main/.d.ts) files and validation logic.

### How does Instatic handle validation errors at the HTTP boundary?

When `readValidatedBody` or `apiRequest` encounters invalid data, they throw an `ApiError` containing the HTTP status code and a formatted message from `formatValueErrors`. Global error handlers catch these exceptions and return standardized JSON error responses to the client, often triggering toast notifications in the UI.

### Where are TypeBox schemas defined in the Instatic codebase?

Schemas are co-located with their usage. HTTP route handlers define request body schemas in files like [`server/handlers/cms/users.ts`](https://github.com/CoreBunch/Instatic/blob/main/server/handlers/cms/users.ts), while persistence schemas reside in [`src/core/persistence/responseSchemas.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/persistence/responseSchemas.ts) and similar files. This colocation ensures that changes to data structures automatically update both the TypeScript types and runtime validators.

### Can validation fail silently in Instatic's persistence layer?

No. Unlike optional runtime type checking, Instatic's persistence layer **requires** successful validation before returning data. The `parseValue` function throws a hard error if stored data does not match the schema, forcing developers to address data corruption immediately rather than propagating `any` types through the application.