# How TypeBox Validation Works at HTTP Boundaries in Instatic

> Learn how Instatic uses TypeBox validation at HTTP boundaries to ensure type-safe data enters your React state via the apiRequest wrapper, preventing errors.

- Repository: [CoreBunch/Instatic](https://github.com/CoreBunch/Instatic)
- Tags: how-to-guide
- Published: 2026-07-31

---

**TLDR:** Instatic validates every HTTP response against a TypeBox schema using the `apiRequest` wrapper, which delegates to `readEnvelope` and `parseJsonResponse` to ensure only type-safe data enters React state, throwing typed `ApiError` instances on validation or HTTP failures.

Instatic enforces a strict architectural rule at the HTTP layer: no data reaches React state without passing through TypeBox validation. This pattern, implemented in the CoreBunch/Instatic repository, centralizes all fetch logic in a canonical HTTP client that compiles TypeBox schemas for runtime validation while preserving static TypeScript types. By routing every network request through [[`apiClient.ts`](https://github.com/CoreBunch/Instatic/blob/main/apiClient.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts), the codebase eliminates untyped boundaries that could introduce runtime errors.

## The Validation Pipeline in apiClient.ts

The entry point for HTTP validation is [[`src/core/http/apiClient.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts), which exports `apiRequest` and `readEnvelope`. These functions form a pipeline where raw `Response` objects are transformed into validated, typed data structures before reaching application logic.

### The readEnvelope Gatekeeper

The `readEnvelope<T>` function serves as the primary validation boundary. It accepts a `Response` object, a TypeBox schema, and a fallback error message. If the response status is not OK, it throws an `ApiError`. Otherwise, it passes the body to `parseJsonResponse` for schema validation.

```typescript
import type { TSchema, Static } from '@sinclair/typebox';
import { parseJsonResponse } from '@core/utils/jsonValidate';

export async function readEnvelope<T extends TSchema>(
  res: Response,
  schema: T,
  fallback: string,
): Promise<Static<T>> {
  if (!res.ok) {
    throw new ApiError(await responseErrorMessage(res, fallback), res.status);
  }
  // TypeBox validation happens here
  return parseJsonResponse(res, schema);
}

```

This pattern ensures that any data flowing from the network to the UI must conform to the schema's shape, with TypeScript's `Static<T>` extracting the compile-time type from the runtime schema.

### Schema Compilation and Caching

Behind the scenes, [`parseJsonResponse`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/jsonValidate.ts) handles the heavy lifting of schema compilation. Located in [`src/core/utils/jsonValidate.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/jsonValidate.ts), this utility compiles the TypeBox schema once using `typeboxCompiler` and caches the validator for performance. When `readEnvelope` calls `parseJsonResponse`, the JSON body is validated against the compiled schema, and a typed result is returned or an error is thrown.

### Error Envelope Validation

Instatic validates error responses with the same rigor as success payloads. The `ErrorEnvelopeSchema` in [`apiClient.ts`](https://github.com/CoreBunch/Instatic/blob/main/apiClient.ts) ensures that server error bodies match the expected `{ error?: unknown }` shape:

```typescript
import { Type } from '@core/utils/typeboxHelpers';

const ErrorEnvelopeSchema = Type.Object(
  { error: Type.Optional(Type.Unknown()) },
  { additionalProperties: true },
);

```

When `responseErrorMessage` processes failed requests, it uses this schema to safely extract error details, preventing malformed error responses from crashing the application.

## apiRequest: The Canonical Wrapper

The `apiRequest` function provides the high-level interface used throughout persistence layers. It configures request credentials, serializes JSON bodies, and delegates response handling to `readEnvelope`:

```typescript
export async function apiRequest<T extends TSchema>(
  url: string,
  options: RequestInit & { schema: T; body?: object }
): Promise<Static<T>> {
  const { schema, body, ...fetchOptions } = options;
  
  const res = await fetch(url, {
    credentials: 'include',
    ...(body && { body: JSON.stringify(body) }),
    ...fetchOptions,
  });
  
  return readEnvelope(res, schema, `Request to ${url} failed`);
}

```

By requiring a `schema` parameter for every request, `apiRequest` makes validation unavoidable, enforcing the architectural rule that every untyped boundary must be validated with TypeBox before reaching React state.

## Architectural Enforcement

The repository maintains validation discipline through automated testing. [[`src/__tests__/architecture/boundary-validation.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/architecture/boundary-validation.test.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/__tests__/architecture/boundary-validation.test.ts) verifies that all HTTP boundaries utilize TypeBox validation, preventing developers from importing raw `fetch` and bypassing the type-safe pipeline. This test ensures that `apiRequest` or `readEnvelope` are the only conduits for network data entering the application.

## Practical Implementation Examples

### Fetching Validated Data

To fetch user data with guaranteed type safety:

```typescript
import { apiRequest } from '@core/http';
import { UserSchema } from '@core/persistence/responseSchemas';

export async function fetchCurrentUser() {
  const user = await apiRequest('/admin/api/cms/users/me', {
    schema: UserSchema,
  });
  // user is typed as Static<typeof UserSchema>
  return user;
}

```

### Handling Validation Errors

Catch `ApiError` instances to differentiate between HTTP failures and schema violations:

```typescript
import { apiRequest, ApiError } from '@core/http';
import { PagesListSchema } from '@core/persistence/responseSchemas';

try {
  const pages = await apiRequest('/admin/api/cms/pages', {
    schema: PagesListSchema,
  });
} catch (error) {
  if (error instanceof ApiError) {
    console.error(`HTTP ${error.status}: ${error.message}`);
    // error.message includes the first TypeBox validation error if applicable
  }
}

```

### Direct readEnvelope Usage

For custom fetch scenarios outside `apiRequest`:

```typescript
import { readEnvelope, ApiError } from '@core/http';
import { ExportResultSchema } from '@core/persistence/responseSchemas';

const response = await fetch('/admin/api/cms/export', { credentials: 'include' });

try {
  const result = await readEnvelope(response, ExportResultSchema, 'Export failed');
  // result is fully typed and validated
} catch (error) {
  if (error instanceof ApiError) {
    // Handles both HTTP errors and malformed JSON bodies
  }
}

```

## Summary

- **Centralized validation**: All HTTP traffic flows through `apiRequest` in [[`apiClient.ts`](https://github.com/CoreBunch/Instatic/blob/main/apiClient.ts)](https://github.com/CoreBunch/Instatic/blob/main/src/core/http/apiClient.ts), ensuring consistent handling.
- **TypeBox integration**: `readEnvelope` validates every response using TypeBox schemas compiled by [`parseJsonResponse`](https://github.com/CoreBunch/Instatic/blob/main/src/core/utils/jsonValidate.ts).
- **Error safety**: Both success and error responses are validated against schemas, with `ApiError` providing typed error information.
- **Architectural enforcement**: Tests in [`boundary-validation.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/boundary-validation.test.ts) prevent unvalidated boundaries from entering the codebase.
- **React state protection**: No network data reaches React state without passing through schema validation, eliminating runtime type errors.

## Frequently Asked Questions

### What happens when a TypeBox schema validation fails?

When `parseJsonResponse` detects a schema mismatch, it generates an error message from the first TypeBox validation failure. `readEnvelope` catches this and throws an `ApiError` with the appropriate status code, preventing invalid data from propagating to React components.

### Why does Instatic use TypeBox instead of Zod?

Instatic adheres to architectural constraint 272, which mandates TypeBox for all validation boundaries. TypeBox schemas are pure JSON Schema-compatible data structures that can be shared across client-server boundaries and external protocols like MCP, while avoiding the bundle size and API surface of banned alternatives.

### How does the architecture test prevent unvalidated HTTP calls?

The [`boundary-validation.test.ts`](https://github.com/CoreBunch/Instatic/blob/main/boundary-validation.test.ts) file statically analyzes imports and function calls to ensure that all network entry points use either `apiRequest` or `readEnvelope` with an explicit TypeBox schema. Any raw `fetch` usage that bypasses these wrappers will trigger a test failure during continuous integration.

### Can readEnvelope handle streaming or non-JSON responses?

No. `readEnvelope` is specifically designed for JSON responses that require TypeBox validation. For streaming data or binary responses, developers should use the standard `fetch` API directly, though these patterns are rare in the admin interface and typically don't require the same runtime type guarantees as JSON API boundaries.