# How OpenSEO Handles Data Structures and Type Safety: Zod, TypeScript, and Drizzle ORM

> Discover how OpenSEO ensures end-to-end type safety using Zod, TypeScript, and Drizzle ORM for robust data integrity from API to database.

- Repository: [Every App/open-seo](https://github.com/every-app/open-seo)
- Tags: internals
- Published: 2026-08-08

---

**OpenSEO enforces end-to-end type safety by combining Zod for runtime validation, TypeScript's static type system with `z.infer`, and Drizzle ORM for database contracts, ensuring data integrity from API requests through to persistent storage.**

OpenSEO (from the [every-app/open-seo](https://github.com/every-app/open-seo) repository) implements a rigorous architecture for managing data structures and type safety across its entire stack. The application uses a layered validation strategy that catches errors at compile-time, runtime, and database boundaries. This approach ensures that incoming API payloads, internal function arguments, and database records maintain strict type contracts without code duplication.

## Runtime Validation with Zod Schemas

OpenSEO uses **Zod** as its primary validation backbone for all external-facing inputs. Every API entry point in `src/serverFunctions/*` and MCP routes begins with a Zod schema that guarantees incoming data conforms to expected shapes before reaching core business logic.

The [createProjectSchema](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts) in [`src/types/schemas/projects.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts) demonstrates advanced validation patterns including custom refinements for DataForSEO location codes and cross-field validation:

```ts
// src/types/schemas/projects.ts
import { z } from "zod";
import {
  isSupportedLanguageCode,
  isSupportedLocationCode,
} from "@/shared/keyword-locations";

export const createProjectSchema = z.object({
  name: z.string().trim().min(1).max(120),
  domain: z.string().trim().max(255).optional(),
  locationCode: z
    .number()
    .int()
    .refine(isSupportedLocationCode, "Unsupported DataForSEO location code")
    .optional(),
  languageCode: z
    .string()
    .refine(isSupportedLanguageCode, "Unsupported language code")
    .optional(),
}).refine(
  ({ locationCode, languageCode }) => locationCode != null || languageCode == null,
  { message: "A language requires a location.", path: ["languageCode"] }
);

// The inferred static type
export type CreateProjectInput = z.infer<typeof createProjectSchema>;

```

Server functions such as [createProjectHandler](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/projects.ts) utilize these schemas to parse untrusted input. The `parse()` method throws descriptive errors for invalid data, while successful parses return fully typed objects:

```ts
// src/serverFunctions/projects.ts
import { z } from "zod";
import { createProjectSchema } from "@/types/schemas/projects";
import { db } from "@/db/client";

export async function createProjectHandler(rawInput: unknown) {
  // Runtime validation – throws if the shape is wrong
  const input = createProjectSchema.parse(rawInput);

  // `input` is now typed as `CreateProjectInput`
  await db.insert(projects).values({
    id: crypto.randomUUID(),
    name: input.name,
    domain: input.domain,
    locationCode: input.locationCode,
    languageCode: input.languageCode,
  });
}

```

Additional schemas like [setProjectMarketSchema](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts) and those in [src/types/schemas/keywords.ts](https://github.com/every-app/open-seo/blob/main/src/types/schemas/keywords.ts) provide the same validation rigor for market configuration and keyword management operations.

## Static Type Inference via z.infer

OpenSEO eliminates type definition duplication by deriving TypeScript types directly from Zod schemas using `z.infer<typeof schema>`. The `CreateProjectInput` type exported from [`src/types/schemas/projects.ts`](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts) flows through the service layer, repository methods, and UI hooks without manual casting or interface maintenance.

This inference strategy ensures that modifying a Zod schema automatically propagates type changes throughout the codebase. When [createProjectHandler](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/projects.ts) parses input, the returned object carries the exact TypeScript type derived from the schema's shape, enabling compile-time autocompletion and refactoring support across server functions and [onboarding](https://github.com/every-app/open-seo/blob/main/src/serverFunctions/onboarding.ts) workflows.

## Database Type Safety with Drizzle ORM

For database-level type contracts, OpenSEO employs **Drizzle ORM** with dialect-specific schema definitions. Table shapes are defined using `sqliteTable` in [src/db/app.schema.ts](https://github.com/every-app/open-seo/blob/main/src/db/app.schema.ts) and `pgTable` in [src/db/pg/app.schema.ts](https://github.com/every-app/open-seo/blob/main/src/db/pg/app.schema.ts), generating TypeScript types that stay synchronized with the underlying SQLite or PostgreSQL tables.

The projects table definition illustrates the declarative schema approach:

```ts
// src/db/app.schema.ts
import { sqliteTable, text, integer } from "drizzle-orm/sqlite-core";

export const projects = sqliteTable("project", {
  id: text("id").primaryKey(),
  name: text("name").notNull(),
  domain: text("domain"),
  locationCode: integer("location_code"),
  languageCode: text("language_code"),
});

```

Repository methods leverage Drizzle's `InferSelectModel` and `InferInsertModel` utilities to type query results and insertion payloads. The [ProjectRepository](https://github.com/every-app/open-seo/blob/main/src/server/features/projects/repositories/ProjectRepository.ts) uses these generated types to ensure type-safe data access:

```ts
// src/server/features/projects/repositories/ProjectRepository.ts
import { db } from "@/db/client";
import { projects } from "@/db/app.schema";
import type { InferSelectModel } from "drizzle-orm";

export type Project = InferSelectModel<typeof projects>;

export async function getProjectById(id: string): Promise<Project | null> {
  return db
    .select()
    .from(projects)
    .where(eq(projects.id, id))
    .get();
}

```

## Cross-Module Consistency and Client-Side Type Safety

OpenSEO maintains a single source of truth by sharing Zod schemas and their inferred types across client, server, and database layers. The same `CreateProjectInput` type used in server validation propagates to React hooks via direct imports from repository files.

The [useProject](https://github.com/every-app/open-seo/blob/main/src/client/hooks/useProject.ts) hook demonstrates this end-to-end type safety by consuming the `Project` type defined in the server-side repository:

```ts
// src/client/hooks/useProject.ts
import { useQuery } from "@tanstack/react-query";
import type { Project } from "@/server/features/projects/repositories/ProjectRepository";

export function useProject(id: string) {
  return useQuery<Project | null>(["project", id], async () => {
    const res = await fetch(`/api/project/${id}`);
    if (!res.ok) throw new Error("Failed to fetch");
    return res.json();
  });
}

```

This architecture ensures identical validation behavior across deployment environments, whether running as a self-hosted Cloudflare worker or within a Docker development container. External API integrations (such as DataForSEO) undergo the same Zod validation before transmission, protecting the system from malformed third-party responses.

## Summary

- **Zod schemas** in `src/types/schemas/*.ts` provide runtime validation for all API inputs, including complex cross-field refinements for external service integrations.
- **TypeScript inference** via `z.infer` generates static types automatically, eliminating duplication between validation logic and type definitions.
- **Drizzle ORM** delivers database-level type safety through `InferSelectModel` and dialect-specific schema files for both SQLite and PostgreSQL.
- **Single source of truth** architecture shares schemas between server functions, repositories, and client hooks, ensuring type consistency across the entire stack.

## Frequently Asked Questions

### What validation library does OpenSEO use for runtime type checking?

OpenSEO uses **Zod** for all runtime validation. Every API entry point in `src/serverFunctions/*` and MCP routes begins with a Zod schema (such as [createProjectSchema](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts)) that validates incoming payloads using `schema.parse()` or `schema.safeParse()`, throwing descriptive errors before invalid data reaches business logic.

### How does OpenSEO generate TypeScript types without duplicating definitions?

OpenSEO leverages `z.infer<typeof schema>` to derive TypeScript types directly from Zod schemas. For example, the `CreateProjectInput` type is inferred from [createProjectSchema](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts), ensuring the static type system stays synchronized with runtime validators automatically without maintaining separate interface definitions.

### How does OpenSEO handle type safety across different database engines?

OpenSEO uses **Drizzle ORM** with dialect-specific schema files like [src/db/app.schema.ts](https://github.com/every-app/open-seo/blob/main/src/db/app.schema.ts) (SQLite) and [src/db/pg/app.schema.ts](https://github.com/every-app/open-seo/blob/main/src/db/pg/app.schema.ts) (Postgres). Drizzle generates `InferSelectModel` and `InferInsertModel` types from these table definitions, providing type-safe queries regardless of whether the deployment targets SQLite or PostgreSQL.

### Where are Zod schemas defined and how are they organized?

Zod schemas are centralized in `src/types/schemas/` with files like [projects.ts](https://github.com/every-app/open-seo/blob/main/src/types/schemas/projects.ts) and [keywords.ts](https://github.com/every-app/open-seo/blob/main/src/types/schemas/keywords.ts). These files export both the validation schemas and their inferred TypeScript types, which are then imported by server functions, repositories, and client hooks to maintain a single source of truth across the application.