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

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 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 in src/types/schemas/projects.ts demonstrates advanced validation patterns including custom refinements for DataForSEO location codes and cross-field validation:

// 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 utilize these schemas to parse untrusted input. The parse() method throws descriptive errors for invalid data, while successful parses return fully typed objects:

// 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 and those in 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 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 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 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 and pgTable in 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:

// 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 uses these generated types to ensure type-safe data access:

// 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 hook demonstrates this end-to-end type safety by consuming the Project type defined in the server-side repository:

// 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) 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, 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 (SQLite) and 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 and 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.

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:

Share the following with your agent to get started:
curl -s "https://instagit.com/install.md"

Works with
Claude Codex Cursor VS Code OpenClaw Any MCP Client

Maintain an open-source project? Get it listed too →